Асоціація рейок з кількома зовнішніми ключами


84

Я хочу мати можливість використовувати два стовпці в одній таблиці для визначення відносин. Отже, використовуючи програму завдань як приклад.

Спроба 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 - асоціація множинних ключових покажчиків

Отже, цей метод буде виглядати так,

Спроба 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

2
Це дорогоцінний камінь, що ви шукаєте? github.com/composite-primary-keys/composite_primary_keys
mus

Дякую за інформацію, але це не те, що я шукаю. Я хочу запит для будь-якого або стовпця, що є заданим значенням. Не складений первинний ключ.
JonathanSimmons

так, оновлення дає зрозуміти. Забудьте про самоцвіт. Ми обидва думали, що ви просто хочете використовувати складений первинний ключ. Це повинно бути можливим, принаймні, шляхом визначення користувацького обсягу співвідношення масштабу. Цікавий сценарій. Я подивлюсь пізніше
dre-hh

FWIW, моя мета тут - отримати задане завдання для користувачів і зберегти формат ActiveRecord :: Relation, щоб я міг продовжувати використовувати області результатів для результатів для пошуку / фільтрації.
Джонатан

Відповіді:


80

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, він буде отримувати результати в одному запиті, і ніякого об’єднання / об’єднання робити не потрібно.
Арслан Алі,

2
Просто FYI, зовнішні ключі, вказані в Taskмоделі, насправді не потрібні. Оскільки у вас є belongs_toвідносини з ім'ям :ownerі :assignee, Rails за замовчуванням буде вважати , що зовнішні ключі називаються owner_idі assignee_id, відповідно.
jeffdill2

1
@ArslanAli немає проблем. :-)
jeffdill2

47

Поширюючись на відповідь @ dre-hh вище, яку, як я виявив, більше не працює, як очікувалося в Rails 5. Здається, Rails 5 тепер включає пункт за замовчуванням where WHERE tasks.user_id = ?, який не працює, оскільки user_idв цьому сценарії немає стовпця.

Я виявив, що все ще можна змусити його працювати з has_manyасоціацією, вам просто потрібно скасувати цей додатковий пункт, доданий Rails.

class User < ApplicationRecord
  has_many :tasks, ->(user) {
    unscope(:where).where(owner: user).or(where(assignee: user)
  }
end

Дякую @Dwight, я б ніколи про це не думав!
Гейб Коплі,

Не вдається змусити це працювати для belongs_to(де батько не має ідентифікатора, тому він повинен базуватися на багатоколонковому ПК). Просто сказано, що відношення рівне нулю (і, дивлячись на консоль, я не бачу, щоб колись виконувався лямбда-запит).
fgblomqvist

@fgblomqvist belong_to має опцію під назвою: primary_key, яка визначає метод, який повертає первинний ключ асоційованого об'єкта, що використовується для асоціації. За замовчуванням це ідентифікатор. Думаю, це може вам допомогти.
Ахмед Камаль

@AhmedKamal Я не бачу, як це взагалі корисно?
fgblomqvist

24

Рейки 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

Оновлення1: Щодо коментаря @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

1
так приголомшливо, скрізь, де я дивився, люди постійно намагалися запропонувати використовувати цей застарілий самоцвіт для композитних ключів
FireDragon

2
Якщо можливо, ви можете розширити цю відповідь, щоб пояснити, що відбувається? Я хотів би це зрозуміти краще, щоб я міг реалізувати щось подібне. подяка
Стівен Лід

1
хоча це зберігає асоціацію, воно спричиняє інші проблеми. з документів: Примітка: Приєднання, бажання завантажувати та попередньо завантажувати ці асоціації не повністю можливо. Ці операції відбуваються до створення екземпляра, і область буде викликана з аргументом nil. Це може призвести до несподіваної поведінки та застаріло. api.rubyonrails.org/classes/ActiveRecord/Associations/…
s2t2

1
Це виглядає багатообіцяючим, але Rails 5 все одно використовує чужу_ключ із whereзапитом, а не просто використовує
лямбду

1
Так, здається, це вже не працює в Rails 5.
Дуайт,

13

Я розробив рішення для цього. Я відкритий для будь-яких вказівок щодо того, як я можу зробити це кращим.

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 і результат - все виконане завдання, що належить або призначене першому користувачеві.


Ви все ще використовуєте цей метод для вирішення свого питання? Я перебуваю в одному човні і цікаво, чи навчилися ви по-новому, чи це все-таки найкращий варіант.
Дан,

Все-таки найкращий варіант, який я знайшов.
Джонатан

Це, мабуть, залишок старих спроб. Відредаговано відповідь, щоб її видалити.
Джонатан

1
Чи буде ця та відповідь dre-hh нижче виконати те саме?
dkniffin

1
> Необхідність передачі об'єкта користувача в область дії на моделі користувача здається зворотним підходом. Вам не потрібно нічого передавати, це лямбда, і поточний екземпляр користувача передається йому автоматично
dre-hh

2

Моя відповідь на Асоціації та (кілька) зовнішніх ключів у рейках (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)

Причина в тому, що rails намагатиметься використовувати current_user.id для заповнення task.user_id, лише щоб виявити, що нічого подібного user_id немає.

Отже, розглядайте мій метод злому як нестандартний спосіб, але не робіть цього.


Не впевнений, що ця відповідь надає щось, що затверджена відповідь ще не робила. Більше того, це схоже на рекламу іншого питання.
Джонатан

@JonathanSimmons Дякую. Я видалю цю відповідь, якщо погано буде діяти як реклама.
sunsoft

2

Оскільки Rails 5 ви також можете зробити те, що є безпечнішим способом ActiveRecord:

def tasks
  Task.where(owner: self).or(Task.where(assignee: self))
end
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.