Chcę mieć możliwość użycia dwóch kolumn w jednej tabeli do zdefiniowania relacji. Na przykład na przykładzie aplikacji zadań.
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
A następnie Task.create(owner_id:1, assignee_id: 2)
To pozwala mi wykonać Task.first.owner
operację, która zwraca użytkownika pierwszego i Task.first.assignee
zwraca użytkownika drugiego, ale User.first.task
nic nie zwraca. Dzieje się tak, ponieważ zadanie nie należy do użytkownika, należą one 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 kończy się niepowodzeniem, ponieważ dwa klucze obce nie wydają się być obsługiwane.
Chcę więc móc powiedzieć User.tasks
i uzyskać zarówno należące do użytkowników, jak i przydzielone im zadania.
Zasadniczo w jakiś sposób zbuduj relację, która równałaby się zapytaniu Task.where(owner_id || assignee_id == 1)
Czy to jest możliwe?
Aktualizacja
Nie chcę używać finder_sql
, ale niedopuszczalna odpowiedź tego problemu wydaje się być bliska tego, czego chcę: Rails - Multiple Index Key Association
Więc ta metoda wyglądałaby następująco:
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ę to uruchomić Rails 4
, pojawia się następujący błąd:
ActiveRecord::PreparedStatementInvalid: missing value for :owner_id in :donor_id => :person_id OR assignee_id => :person_id
źródło
Odpowiedzi:
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
wUser
klasie.Używanie w
has_many :tasks
ogóle nie ma sensu, ponieważ nie mamy żadnej kolumny nazwanejuser_id
w tabelitasks
.To, co zrobiłem, aby rozwiązać problem w moim przypadku, to:
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 nazwać
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
iowned_tasks
.To mogłoby być dobre rozwiązanie, jeśli chodzi o czytelność, ale z punktu widzenia wydajności nie byłoby tak dobre, jak teraz, aby uzyskać
tasks
dwa zapytania zamiast raz, a następnie wynik z tych dwóch zapytań również należy połączyć.Aby więc pobrać zadania należące do użytkownika, zdefiniowalibyśmy
tasks
wUser
klasie własną metodę 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 scalać ani łączyć żadnych wyników.
źródło
has_many
„bez sensu” Jeśli przeczytasz zaakceptowaną odpowiedź, zobaczysz, że w ogóle nie używaliśmyhas_many
deklaracji. Zakładając, że Twoje wymagania odpowiadają potrzebom każdego innego gościa, jest to bezproduktywne, ta odpowiedź mogłaby być mniej osądzająca.owned_tasks
iassigned_tasks
do uzyskania wszystkich zadań będzie miało wskazówkę dotyczącą wydajności. W każdym razie zaktualizowałem moją odpowiedź i umieściłem metodę wUser
klasie, aby uzyskać wszystkie powiązanetasks
, pobierze wyniki w jednym zapytaniu i nie trzeba wykonywać scalania / łączenia.Task
modelu nie są w rzeczywistości potrzebne. Ponieważ maszbelongs_to
relacje o nazwach:owner
i:assignee
, Railsy domyślnie przyjmą, że klucze obce mają odpowiednio nazwyowner_id
iassignee_id
.Rozszerzając powyższą odpowiedź @ dre-hh, która według mnie nie działa już zgodnie z oczekiwaniami w Railsach 5. Wygląda na to, że Rails 5 zawiera teraz domyślną klauzulę where dla efektu
WHERE tasks.user_id = ?
, która zawodzi, ponieważuser_id
w tym scenariuszu nie ma kolumny.Odkryłem, że nadal jest możliwe, aby działał z
has_many
asocjacją, wystarczy usunąć tę dodatkową klauzulę where dodaną przez Railsy.class User < ApplicationRecord has_many :tasks, ->(user) { unscope(:where).where(owner: user).or(where(assignee: user) } end
źródło
belongs_to
(gdzie rodzic nie ma identyfikatora, więc musi być oparty na wielokolumnowym PK). Po prostu mówi, że relacja jest zerowa (i patrząc na konsolę, nie widzę, aby zapytanie lambda kiedykolwiek zostało wykonane).Szyny 5:
musisz wyłączyć domyślną klauzulę where, zobacz odpowiedź @Dwight, jeśli nadal chcesz powiązać has_many.
Chociaż
User.joins(:tasks)
daje miArgumentError: 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ż skorzystać z rozwiązania @Arslan Ali.
Szyny 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
Aktualizacja 1: Odnośnie komentarza @JonathanSimmons
Nie musisz przekazywać modelu użytkownika do tego zakresu. Bieżąca instancja użytkownika jest przekazywana automatycznie do tej lambdy. Nazwij to tak:
user = User.find(9001) user.tasks
Aktualizacja2:
Wywołanie
has_many :tasks
klasy ActiveRecord zapisze funkcję lambda w jakiejś zmiennej klasy i jest po prostu fantazyjnym sposobem na wygenerowanietasks
metody na swoim obiekcie, która wywoła tę lambdę. 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
źródło
where
zapytaniem zamiast po prostu używać lambdy powyżejWypracował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 zasadniczo przesłania asocjację has_many, ale nadal zwraca
ActiveRecord::Relation
obiekt, którego szukałem.Więc teraz mogę zrobić coś takiego:
User.first.tasks.completed
a wynikiem jest ukończone zadanie należące do pierwszego użytkownika lub przypisane do niego.źródło
Moja odpowiedź na asocjacje i (wiele) klucze obce w railsach (3.2): jak je opisać 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, nie rób tego, co sugerowałem, ponieważ ten hack spowoduje problem, gdy zrobisz coś takiego:
Powodem jest to, że railsy będą próbowały użyć current_user.id do wypełnienia zadania task.user_id, tylko po to, aby stwierdzić, że nie istnieje nic takiego jak user_id.
Więc potraktuj moją metodę hackowania jako wyjście poza schemat, ale nie rób tego.
źródło
Od Rails 5 możesz także zrobić to, co jest bezpieczniejszym sposobem ActiveRecord:
def tasks Task.where(owner: self).or(Task.where(assignee: self)) end
źródło