Подтвердить что ты не робот

Объединение 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
4b9b3361

Ответ 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 нет.

Итак, рассмотрите мой метод взлома как способ вне поля, но не делайте этого.