Я хочу иметь возможность использовать два столбца в одной таблице для определения отношения. Итак, на примере приложения-задачи.
Попытка 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
источник
Ответы:
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
Таким образом, он получит все результаты в одном запросе, и нам не придется объединять или объединять какие-либо результаты.
источник
has_many
«бессмысленности». Если вы прочитаете принятый ответ, то увидите, что мы вообще не использовалиhas_many
декларацию. Если предположить, что ваши требования соответствуют потребностям любого другого посетителя, это непродуктивно, этот ответ мог бы быть менее осуждающим.owned_tasks
иassigned_tasks
для получения всех задач будет иметь подсказку по производительности. В любом случае, я обновил свой ответ и включил вUser
класс метод для получения всего связанногоtasks
, он будет получать результаты в одном запросе, и никакого слияния / комбинирования выполнять не нужно.Task
модели, на самом деле не нужны. Поскольку у вас естьbelongs_to
отношения с именами:owner
и:assignee
, Rails по умолчанию предполагает, что внешние ключи имеют именаowner_id
иassignee_id
, соответственно.Расширение ответа @ dre-hh, приведенного выше, который, как я обнаружил, больше не работает должным образом в Rails 5. Похоже, что Rails 5 теперь включает предложение where по умолчанию для эффекта
WHERE tasks.user_id = ?
, которое не работает, посколькуuser_id
в этом сценарии нет столбца.Я обнаружил, что все еще возможно заставить его работать с
has_many
ассоциацией, вам просто нужно отменить это дополнительное предложение where, добавленное Rails.class User < ApplicationRecord has_many :tasks, ->(user) { unscope(:where).where(owner: user).or(where(assignee: user) } end
источник
belongs_to
(где у родителя нет идентификатора, поэтому он должен быть основан на многоколоночном ПК). Он просто говорит, что отношение равно нулю (и, глядя на консоль, я не вижу, чтобы лямбда-запрос когда-либо выполнялся).Рельсы 5:
вам нужно отменить область действия по умолчанию, см. ответ @Dwight, если вам все еще нужна ассоциация has_many.
Хотя
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
источник
where
запросом, а не просто лямбда,Я разработал решение для этого. Я открыт для любых указаний о том, как я могу это улучшить.
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
и в результате вся выполненная задача принадлежит или назначена первому пользователю.источник
Мой ответ ассоциациям и (множественным) внешним ключам в рельсах (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 и вам нужно создать новую запись или отредактировать существующую, пожалуйста, не делайте того, что я предлагал, потому что этот хак вызовет проблемы, когда вы сделаете что-то вроде этого:
Причина в том, что rails попытается использовать current_user.id для заполнения task.user_id, но обнаружит, что нет ничего похожего на user_id.
Итак, рассматривайте мой метод взлома как выход за рамки стандартного, но не делайте этого.
источник
Начиная с Rails 5 вы также можете делать то, что является более безопасным способом ActiveRecord:
def tasks Task.where(owner: self).or(Task.where(assignee: self)) end
источник