Объединение Rails с несколькими внешними ключами
Я хочу иметь возможность использовать два столбца в одной таблице для определения отношений. Таким образом, использование приложения-задачи в качестве примера.
Попытка 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
, который возвращает user one и 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)
Возможно ли это?
Update
Я не хочу использовать 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
Ответы
Ответ 1
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
.
Использование 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
, два раза будут выдаваться два запроса, и тогда результат этих двух запросов также должен быть объединен.
Итак, чтобы получить задачи, принадлежащие пользователю, мы определяем собственный tasks
метод в классе User
следующим образом:
def tasks
Task.where("owner_id = ? OR assigneed_id = ?", self.id, self.id)
end
Таким образом, он будет получать все результаты в одном запросе, и нам не нужно было бы объединять или комбинировать любые результаты.
Ответ 2
Рельсы 5:
вам нужно отменить предложение по умолчанию, где см. ответ @Dwight, если вы все еще хотите иметь has_many associaiton.
Хотя 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 :tasks
в классе ActiveRecord будет хранить лямбда-функцию в некоторой переменной класса и является просто причудливым способом генерации метода 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
Ответ 3
Я разработал для этого решение. Я открыт для любых указателей на то, как я могу сделать это лучше.
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
, и результатом будет все завершенная задача, принадлежащая или назначенная первому пользователю.
Ответ 4
Расширение ответа @dre-hh выше, которое, как я нашел, больше не работает, как ожидалось, в Rails 5. Появляется Rails 5 теперь включает предложение по умолчанию, в котором действует WHERE tasks.user_id = ?
, что не работает, поскольку нет user_id
в этом сценарии.
Я нашел, что все еще возможно заставить его работать с ассоциацией has_many
, вам просто нужно отменить это дополнительное предложение where, добавленное Rails.
class User < ApplicationRecord
has_many :tasks, ->(user) { unscope(:where).where("owner_id = :id OR assignee_id = :id", id: user.id) }
end
Ответ 5
Мой ответ на Ассоциации и (несколько) внешних ключей в рельсах (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)
Причина в том, что рельсы будут пытаться использовать current_user.id для заполнения task.user_id, только чтобы найти, что ничего подобного user_id нет.
Итак, рассмотрите мой метод взлома как способ вне поля, но не делайте этого.