Szyny z wieloma kluczami zagranicznymi

Chcę być w stanie użyć dwóch kolumn na jednej tabeli do zdefiniowania relacji. Więc za pomocą aplikacji Zadań jako przykład.

Próba 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

Więc Task.create(owner_id:1, assignee_id: 2)

To pozwala mi wykonać Task.first.owner który zwraca user one i Task.first.assignee który zwraca user two Ale User.first.task nie zwraca nic. Ponieważ zadanie nie należy do użytkownika, należy do właściciela i cesjonariusza . Więc

Próba 2:

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

class Task < ActiveRecord::Base
  belongs_to :user
end

To po prostu całkowicie zawodzi, ponieważ dwa klucze obce nie wydają się być obsługiwane.

Chcę więc być w stanie powiedzieć User.tasks i uzyskać zarówno własne, jak i przypisane zadania.

W zasadzie jakoś zbudować relację, która byłaby równa zapytaniu Task.where(owner_id || assignee_id == 1)

Czy to możliwe?

Update

Nie chcę używać finder_sql, ale odpowiedź tego problemu wygląda na zbliżoną do tego, czego chcę: Rails - Multiple Index Key Stowarzyszenie

Więc ta metoda wyglądałaby tak,

Próba 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

Chociaż mogę go uruchomić w Rails 4, ciągle dostaję następujący błąd:

ActiveRecord::PreparedStatementInvalid: missing value for :owner_id in :donor_id => :person_id OR assignee_id => :person_id
Author: Community, 2014-07-09

5 answers

TL;DR

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

Usuń has_many :tasks w klasie User.


Użycie has_many :tasks w ogóle nie ma sensu, ponieważ nie mamy żadnej kolumny o nazwie user_id w tabeli tasks.

Co zrobiłem, aby rozwiązać problem w moim przypadku jest:

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

W ten sposób można wywołać User.first.assigned_tasks jak również User.first.owned_tasks.

Teraz możesz zdefiniować metodę o nazwie tasks, która zwraca kombinację assigned_tasks i owned_tasks.

To może być dobre rozwiązanie jeśli chodzi o czytelność, ale z z punktu widzenia wydajności, nie byłoby to tak dobre jak teraz, aby uzyskać tasks, dwa zapytania zostaną wydane zamiast raz, a następnie wynik tych dwóch zapytań należy również połączyć.

Więc aby uzyskać zadania, które należą do użytkownika, zdefiniujemy niestandardową metodę tasks w klasie User w następujący sposób:

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

W ten sposób pobierze wszystkie wyniki w jednym zapytaniu i nie będziemy musieli łączyć ani łączyć żadnych wyników.

 63
Author: Arslan Ali,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-02-05 00:10:49

Rails 5:

Musisz rozszyfrować domyślną klauzulę where zobacz odpowiedź @Dwight, jeśli nadal chcesz mieć asocjację has_many.

Choć User.joins(:tasks) daje mi

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

Ponieważ nie jest to już możliwe, Możesz również użyć @Arslan Ali solution.

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: Regarding @ JonathanSimmons comment

Przekazanie obiektu user do scope w modelu User wydaje się być podejściem wstecznym

Nie masz aby przekazać model użytkownika do tego zakresu. Bieżąca instancja użytkownika jest przekazywana automatycznie do tej lambda. Nazwij to tak:

user = User.find(9001)
user.tasks

Update2:

Jeśli to możliwe, czy mógłbyś rozwinąć tę odpowiedź, aby wyjaśnić, co się dzieje? Chciałbym to lepiej zrozumieć, aby móc wdrożyć coś podobnego. dzięki

Wywołanie has_many :tasks w klasie ActiveRecord zapisze funkcję lambda w jakiejś zmiennej klasy i jest po prostu fantazyjnym sposobem na wygenerowanie metody tasks na jej obiekcie, która zadzwonię do lambdy. Wygenerowana metoda wyglądałaby podobnie do następującego pseudokodu:

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
 21
Author: dre-hh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-07-16 11:38:28

Wymyśliłem na to rozwiązanie. Jestem otwarty na wszelkie wskazówki, jak Mogę to poprawić.

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

To w zasadzie nadpisuje asocjację has_many, ale nadal zwraca obiekt ActiveRecord::Relation, którego szukałem.

Więc teraz mogę zrobić coś takiego:

User.first.tasks.completed a wynikiem jest całe ukończone zadanie należące lub przypisane do pierwszego użytkownika.

 14
Author: JonathanSimmons,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-10-03 22:39:33

Rozszerzenie na odpowiedź @dre-hh powyżej, która już nie działa zgodnie z oczekiwaniami w Rails 5. Wygląda na to, że Rails 5 zawiera teraz domyślną klauzulę where na skutek WHERE tasks.user_id = ?, która nie działa, ponieważ w tym scenariuszu nie ma kolumny user_id.

I ' ve found it is still possible to get it working with a has_many association, you just need to unscope this additional where clause added by Rails.

class User < ApplicationRecord
  has_many :tasks, ->(user) { unscope(:where).where("owner_id = :id OR assignee_id = :id", id: user.id) }
end
 14
Author: Dwight,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-11-07 02:14:59

Moja odpowiedź na skojarzenia i (wiele) kluczy obcych w rails (3.2): jak opisać je w modelu i zapisać migracje jest właśnie dla Ciebie!

Jeśli chodzi o Twój kod,oto moje modyfikacje

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

Ostrzeżenie: Jeśli używasz RailsAdmin i chcesz utworzyć nowy rekord lub edytować istniejący rekord, nie rób tego, co sugeruję.Bo ten hack sprawi problem jak zrobisz coś takiego:

current_user.tasks.build(params)

Powodem jest to, że rails będzie próbował użycie current_user.id aby wypełnić zadanie.user_id, tylko po to, aby stwierdzić, że nie ma czegoś takiego jak user_id.

Więc potraktuj moją metodę hakowania jako wyjście poza pole, ale nie rób tego.
 0
Author: sunsoft,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 11:54:41