여러 외래 키와 Rails 연결


84

한 테이블에 두 개의 열을 사용하여 관계를 정의 할 수 있기를 원합니다. 따라서 작업 앱을 예로 사용합니다.

시도 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반환하는 사용자를 하고 Task.first.assignee반환하는 사용자이 있지만 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

두 개의 외래 키가 지원되지 않는 것처럼 보이므로 모두 실패합니다.

그래서 내가 원하는 것은 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

2
이 보석이 당신이 찾고있는 것입니까? github.com/composite-primary-keys/composite_primary_keys
— mus

정보 mus에 감사하지만 이것은 내가 찾고있는 것이 아닙니다. 주어진 값이되는 또는 열에 대한 쿼리를 원합니다. 복합 기본 키가 아닙니다.
— JonathanSimmons

예, 업데이트를 통해 명확 해집니다. 보석은 잊어 버려. 우리는 둘 다 구성된 기본 키를 사용하기를 원한다고 생각했습니다. 이는 최소한 사용자 지정 범위를 범위 관계로 정의하여 가능해야합니다. 흥미로운 시나리오. 나중에 그것을보고해야합니다
— DRE-HH에게

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 :tasks에서 제거하십시오 User.


table에 has_many :tasks이름이 지정된 열이 없기 때문에 사용 은 전혀 의미가 없습니다 .user_idtasks

내 경우 문제를 해결하기 위해 내가 한 일은 다음과 같습니다.

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_tasksowned_tasks

가독성에 관한 한 좋은 해결책이 될 수 있지만 성능 관점에서 볼 때 지금만큼 좋지 않을 것입니다. tasks , 두 개의 쿼리 대신 한 번으로 발급됩니다, 다음, 결과 이 두 쿼리 중 또한 조인되어야합니다.

따라서 사용자에게 속한 작업을 가져 오기 위해 다음과 같은 방식으로 클래스에서 사용자 지정 tasks메서드를 정의합니다 User.

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

이렇게하면 단일 쿼리로 모든 결과를 가져 오므로 결과를 병합하거나 결합 할 필요가 없습니다.


이 솔루션은 훌륭합니다! 이는 각각 할당, 소유 및 모든 작업에 대한 별도의 액세스에 대한 요구 사항을 의미합니다. 대규모 프로젝트에서 좋은 예감이 될 것입니다. 그러나 그것은 내 경우에는 요구 사항이 아닙니다. has_many"이치에 맞지 않음"에 관해서 받아 들여진 답변을 읽으면 우리는 has_many선언을 전혀 사용하지 않았 음 을 알 수 있습니다. 귀하의 요구 사항이 다른 모든 방문자의 요구 사항과 일치한다고 가정하면이 답변은 덜 판단적일 수 있습니다.
— JonathanSimmons

이것이 우리가이 문제를 가진 사람들에게 옹호해야 할 더 나은 장기적인 설정이라고 생각합니다. 다른 모델의 기본 예와 결합 된 작업 집합을 검색하는 사용자 방법을 포함하도록 답변을 수정하려면 선택한 답변으로 수정하겠습니다. 감사!
— JonathanSimmons

그래 나도 너와 같은 생각이야. 사용 owned_tasks및 assigned_tasks성능 힌트를해야합니다 모든 작업을 얻을 수 있습니다. 어쨌든, 나는 내 대답을 업데이트하고 User모든 관련 tasks을 가져 오기 위해 클래스에 메서드를 포함 시켰 으며 결과를 하나의 쿼리로 가져 오며 병합 / 결합을 수행 할 필요가 없습니다.
— 아르 슬란 알리

2
참고로 Task모델에 지정된 외래 키 는 실제로 필요하지 않습니다. 및 belongs_to라는 관계 가 있으므로 Rails는 기본적으로 외래 키의 이름이 각각 및 라고 가정합니다 . :owner:assigneeowner_idassignee_id
— jeffdill2

1
@ArslanAli 문제 없습니다. :-)
— jeffdill2

47

위의 @ dre-hh의 답변을 확장하면 Rails 5에서 더 이상 예상대로 작동하지 않는 것으로 나타났습니다. Rails 5에는 이제의 효과에 대한 기본 where 절이 포함되어 있으며이 시나리오 WHERE tasks.user_id = ?에는 user_id열 이 없으므로 실패합니다 .

has_many연관 과 함께 작동하는 것이 여전히 가능 하다는 것을 알았습니다. Rails에서 추가 한이 추가 where 절의 범위를 해제하면됩니다.

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

감사합니다 @Dwight, 나는 이것을 결코 생각하지 못했을 것입니다!
— Gabe Kopley

belongs_to(부모에 ID가 없으므로 다중 열 PK를 기반으로해야하는 경우) 이 작업을 수행 할 수 없습니다 . 관계가 nil이라고 만 말합니다 (콘솔을 보면 람다 쿼리가 실행되는 것을 볼 수 없습니다).
— fgblomqvist

@fgblomqvist belongs_to에는 : primary_key라는 옵션이 있습니다.이 옵션은 연결에 사용되는 관련 개체의 기본 키를 반환하는 메서드를 지정합니다. 기본적으로 이것은 id입니다. 나는 이것이 당신을 도울 수 있다고 생각합니다.
— Ahmed Kamal

@AhmedKamal 나는 그것이 전혀 유용하지 않다는 것을 알지 못합니까?
— fgblomqvist

24

레일즈 5 :

여전히 has_many 연관성을 원한다면 @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 솔루션도 사용할 수 있습니다.

레일스 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

업데이트 2 :

가능하다면이 답변을 확장하여 무슨 일이 일어나고 있는지 설명 할 수 있습니까? 비슷한 것을 구현할 수 있도록 더 잘 이해하고 싶습니다. 감사

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
정말 대단, 다른 곳 난 사람들이 복합 키의이 오래된 보석을 사용하는 것이 좋습니다 오려고 보았다
— FireDragon

2
가능하다면 무슨 일이 일어나고 있는지 설명하기 위해이 답변을 확장 할 수 있습니까? 비슷한 것을 구현할 수 있도록 더 잘 이해하고 싶습니다. 감사합니다
— 스티븐 리드

1
이것은 연관성을 유지하지만 다른 문제를 유발합니다. 문서에서 : 참고 : 이러한 연관의 조인, 즉시로드 및 사전로드는 완전히 불가능합니다. 이러한 작업은 인스턴스 생성 전에 발생하며 범위는 nil 인수로 호출됩니다. 이로 인해 예상치 못한 동작이 발생할 수 있으며 더 이상 사용되지 않습니다. api.rubyonrails.org/classes/ActiveRecord/Associations/…
— s2t2

1
약속하지만, 레일 5 개 끝을 여전히 함께 foreign_key 사용이 외모 where쿼리를 대신 바로 위의 람다를 사용하여
— 모하메드 엘 Mahallawy

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 결과는 모든 완료된 작업이 소유하거나 첫 번째 사용자에게 할당됩니다.


여전히이 방법을 사용하여 질문을 해결하고 있습니까? 나는 같은 배를 타고 새로운 방법을 배웠는지 아니면 이것이 여전히 최선의 선택인지 궁금합니다.
— Dan

여전히 내가 찾은 최고의 옵션입니다.
— JonathanSimmons

그것은 아마도 오래된 시도의 잔재 일 것입니다. 그것을 제거하기 위해 답변을 편집했습니다.
— JonathanSimmons

1
이것과 아래의 dre-hh의 대답이 같은 일을 할 수 있습니까?
— dkniffin 2015 년

1
> 사용자 개체를 사용자 모델의 범위로 전달해야하는 것은 역방향 접근 방식처럼 보입니다. 아무것도 전달할 필요가 없습니다. 이것은 람다이고 현재 사용자 인스턴스가 자동으로 여기에 전달됩니다.
— dre-hh

2

Rails (3.2)의 Associations 및 (여러) 외래 키에 대한 나의 대답 : 모델에서 그들을 설명하고 마이그레이션을 작성하는 방법 은 당신을위한 것입니다!

귀하의 코드는 다음과 같습니다.

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가 task.user_id를 채우기 위해 current_user.id를 사용하려고 시도하지만 user_id와 같은 것이 없다는 것을 발견하기 때문입니다.

따라서 내 해킹 방법을 상자 밖의 방법으로 생각하되 그렇게하지 마십시오.


이 답변이 승인 된 답변이 아직 제공하지 않은 내용을 제공하는지 확실하지 않습니다. 또한 다른 질문에 대한 광고처럼 느껴집니다.
— JonathanSimmons

@JonathanSimmons 감사합니다. 광고처럼 행동하는 것이 나쁘면이 답변을 제거하겠습니다.
— sunsoft

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.