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


84

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)

これにより、Task.first.ownerどちらがユーザー1Task.first.assigneeを返し、どちらがユーザー2を返すが、User.first.task何も返さないを実行できます。どのタスクがユーザーに属していないため、彼らはに属し、所有者および譲受人。そう、

試行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-複数のインデックスキーの関連付け

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

試行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

2
この宝石はあなたが探しているものですか?github.com/composite-primary-keys/composite_primary_keys
mus

info musに感謝しますが、これは私が探しているものではありません。または列のいずれかに対するクエリが指定された値である必要があります。複合主キーではありません。
JonathanSimmons

ええ、アップデートはそれを明らかにします。宝石を忘れてください。作成された主キーを使用したいだけだと私たちは考えました。これは、少なくともカスタムスコープとスコープ関係を定義することで可能になるはずです。興味深いシナリオ。後で見ていきます
dre-hh 2014

FWIWここでの私の目標は、特定のユーザータスクを取得し、ActiveRecord :: Relation形式を保持して、検索/フィルタリングの結果でタスクスコープを引き続き使用できるようにすることです。
JonathanSimmons

回答:


80

TL; DR

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

クラスで削除has_many :tasksUserます。


テーブルにhas_many :tasks名前が付けられuser_idた列がないため、使用してもまったく意味がありませんtasks

私の場合、問題を解決するために私がしたことは次のとおりです。

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_tasksと同様に呼び出すことができますUser.first.owned_tasks

さて、あなたはと呼ばれるメソッドを定義することができtasks、そのリターンの組み合わせassigned_tasksとをowned_tasks

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

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

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

このように、1つのクエリですべての結果をフェッチし、結果をマージまたは結合する必要はありません。


このソリューションは素晴らしいです!これは、タスクをそれぞれ割り当て、所有し、すべてに個別にアクセスする必要があることを意味します。大規模なプロジェクトでは、それは素晴らしい予見です。しかし、私の場合、それは要件ではありませんでした。has_manyあなたは私たちが使用して終了していない参照してくださいね受け入れ答え読めば「センスをしていない」has_manyすべての宣言を。あなたの要件が他のすべての訪問者のニーズと一致すると仮定すると、この答えはあまり判断力がなかったかもしれません。
JonathanSimmons

そうは言っても、これは私たちがこの問題を抱えている人々に提唱すべきより良い長期的な設定であると私は信じています。他のモデルの基本的な例と、タスクの組み合わせセットを取得するためのユーザーメソッドを含めるように回答を修正する場合は、選択した回答として修正します。ありがとう!
JonathanSimmons

はい、あなたに賛成です。owned_tasksおよびassigned_tasksを使用してすべてのタスクを取得すると、パフォーマンスのヒントが得られます。とにかく、私は私の答えを更新Userし、関連するすべてを取得するためのメソッドをクラスに含めましたtasks。それは1つのクエリで結果をフェッチし、マージ/結合を行う必要はありません。
Arslan Ali

2
参考までに、Taskモデルで指定された外部キーは実際には必要ありません。あなたが持っているので、belongs_to名前の関係を:owner:assignee、Railsは外部キーが命名されていることをデフォルトで仮定しますowner_idassignee_id、それぞれ。
jeffdill2 2015

1
@ArslanAli問題ありません。:-)
jeffdill2 2015

47

上記の@ dre-hhの回答を拡張すると、Rails 5では期待どおりに機能しなくなったことがわかりました。Rails5WHERE tasks.user_id = ?にはuser_id、このシナリオには列がないため失敗する、の効果に対するデフォルトのwhere句が含まれているようです。

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

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

@Dwightに感謝します、私はこれについて考えたことはありませんでした!
Gabe Kopley 2017

これをaに対して機能させることはできませんbelongs_to(親にはIDがないため、複数列のPKに基づいている必要があります)。リレーションがnilであるとだけ表示されます(コンソールを見ると、ラムダクエリが実行されているのがわかりません)。
fgblomqvist

@fgblomqvistのbelongs_toには、:primary_keyというオプションがあります。このオプションは、関連付けに使用される関連付けられたオブジェクトの主キーを返すメソッドを指定します。デフォルトでは、これはidです。これはあなたを助けることができると思います。
AhmedKamal19年

@AhmedKamalそれがどのように役立つのかまったくわかりませんか?
fgblomqvist

24

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.

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

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.find(9001)
user.tasks

Update2:

可能であれば、この回答を拡張して、何が起こっているのかを説明できますか?似たようなものを実装できるように、もっとよく理解したいと思います。ありがとう

has_many :tasksActiveRecordクラスを呼び出すと、ラムダ関数がクラス変数に格納され、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

1
とても
素晴らしく

2
可能であれば、この回答を拡張して、何が起こっているのかを説明できますか?似たようなものを実装できるように、もっとよく理解したいと思います。ありがとう
スティーブンリード

1
これは関連付けを保持しますが、他の問題を引き起こします。ドキュメントから:注:これらの関連付けの結合、積極的な読み込み、および事前読み込みは完全には不可能です。これらの操作はインスタンスの作成前に行われ、スコープはnil引数で呼び出されます。これは予期しない動作につながる可能性があり、非推奨です。 api.rubyonrails.org/classes/ActiveRecord/Associations/…– s2t2 2017
1

1
これは有望に見えますが、Rails 5whereでは、上記のラムダを使用するのではなく、クエリでforeign_keyを使用することになります
Mohamed El Mahallawy 2017年

1
ええ、これはRails 5では機能しなくなったようです
Dwight

13

私はこれに対する解決策を考え出しました。私はこれをより良くする方法についてのあらゆる指針を受け入れています。

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 結果は、最初のユーザーが所有または割り当てたすべての完了したタスクです。


あなたはまだあなたの質問を解決するためにこの方法を使用していますか?私は同じ船に乗っており、あなたが新しい方法を学んだのか、それともこれが依然として最良の選択肢であるのか疑問に思っています。
ダン

それでも私が見つけた最良の選択肢です。
JonathanSimmons

それはおそらく古い試みの名残です。回答を編集して削除しました。
JonathanSimmons

1
これと以下のdre-hhの答えは同じことを達成しますか?
dkniffin 2015

1
>ユーザーオブジェクトをユーザーモデルのスコープに渡す必要があるのは、逆方向のアプローチのようです。何も渡す必要はありません。これはラムダであり、現在のユーザーインスタンスが自動的に渡されます
dre-hh 2015

2

アソシエーションと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のようなものがないことを検出するためです。

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


この回答が、承認された回答がまだ提供していないものを提供するかどうかはわかりません。また、別の質問の宣伝のように感じます。
JonathanSimmons

@JonathanSimmonsありがとうございます。広告のように振る舞うのが悪い場合は、この回答を削除します。
サンソフト

2

Rails 5以降、ActiveRecordのより安全な方法である方法も実行できます。

def tasks
  Task.where(owner: self).or(Task.where(assignee: self))
end
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.