tablas tabla relaciones rails polimorficas migraciones llave intermedia foranea crear comando belong association asociaciones ruby-on-rails ruby-on-rails-4 associations model-associations

ruby on rails - tabla - Asociación de rieles con múltiples claves foráneas



relaciones de tablas en ruby on rails (5)

Quiero poder usar dos columnas en una tabla para definir una relación. Entonces, usando una aplicación de tareas como ejemplo.

Intento 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

Entonces Task.create(owner_id:1, assignee_id: 2)

Esto me permite realizar Task.first.owner que devuelve user one y Task.first.assignee que devuelve el usuario dos pero User.first.task no devuelve nada. Lo cual se debe a que la tarea no pertenece a un usuario, pertenecen al propietario y al cesionario . Asi que,

Intento 2:

class User < ActiveRecord::Base has_many :tasks, foreign_key: [:owner_id, :assignee_id] end class Task < ActiveRecord::Base belongs_to :user end

Eso simplemente falla, ya que dos claves externas no parecen ser compatibles.

Entonces, lo que quiero es poder decir User.tasks y obtener tanto las tareas User.tasks como User.tasks los usuarios.

Básicamente, de alguna manera, construye una relación que sería igual a una consulta de Task.where(owner_id || assignee_id == 1)

¿Es eso posible?

Actualizar

No estoy buscando usar finder_sql , pero la respuesta no aceptada de este tema parece estar cerca de lo que quiero: Rails - Multiple Index Key Association

Entonces este método se vería así,

Intento 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

Aunque puedo hacer que funcione en Rails 4 , sigo recibiendo el siguiente error:

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

Eliminar has_many :tasks en User clase de User .

Usar has_many :tasks no tiene ningún sentido ya que no tenemos ninguna columna llamada user_id en tasks tabla.

Lo que hice para resolver el problema en mi caso es:

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

De esta forma, puede llamar a User.first.assigned_tasks así como a User.first.owned_tasks .

Ahora, puede definir un método llamado tasks que devuelve la combinación de owned_tasks y owned_tasks .

Esa podría ser una buena solución en lo que respecta a la legibilidad, pero desde el punto de vista del rendimiento, no sería tan bueno como ahora, para obtener las tasks , se emitirán dos consultas en lugar de una, y luego, el el resultado de esas dos consultas también debe unirse.

Por lo tanto, para obtener las tareas que pertenecen a un usuario, definiríamos un método de tasks personalizado en User clase de User de la siguiente manera:

def tasks Task.where("owner_id = ? OR assigneed_id = ?", self.id, self.id) end

De esta forma, obtendrá todos los resultados en una sola consulta, y no tendríamos que combinar ni combinar ningún resultado.


Encontré una solución para esto. Estoy abierto a cualquier sugerencia sobre cómo puedo mejorar esto.

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

Esto básicamente anula la asociación has_many pero aún devuelve el objeto ActiveRecord::Relation que estaba buscando.

Entonces ahora puedo hacer algo como esto:

User.first.tasks.completed y el resultado es toda la tarea completa, propiedad o asignada al primer usuario.


Extendiendo la respuesta de @ dre-hh anterior, que encontré que ya no funciona como se esperaba en Rails 5. Parece que Rails 5 ahora incluye una cláusula where predeterminada donde WHERE tasks.user_id = ? , que falla ya que no hay una columna user_id en este escenario.

Descubrí que todavía es posible hacer que funcione con una asociación has_many , solo tiene que leer esta cláusula where adicional añadida por Rails.

class User < ApplicationRecord has_many :tasks, ->(user) { unscope(:where).where("owner_id = :id OR assignee_id = :id", id: user.id) } end


Mi respuesta a Asociaciones y (múltiples) claves foráneas en rieles (3.2): ¡cómo describirlas en el modelo y escribir migraciones es para ti!

En cuanto a su código, aquí están mis modificaciones

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

Advertencia: si está utilizando RailsAdmin y necesita crear un nuevo registro o editar un registro existente, no haga lo que le he sugerido. Porque este truco causará problemas cuando haga algo como esto:

current_user.tasks.build(params)

La razón es que los raíles intentarán usar current_user.id para completar task.user_id, solo para encontrar que no hay nada como user_id.

Por lo tanto, considere mi método de pirateo como una forma de salir de la caja, pero no lo haga.


Rails 5:

necesita desaprender la cláusula predeterminada donde vea @Dwight answer si aún desea una asociación has_many.

Aunque User.joins(:tasks) me da

ArgumentError: The association scope ''tasks'' is instance dependent (the scope block takes an argument). Preloading instance dependent scopes is not supported.

Como ya no es posible, puede usar la solución de @Arslan Ali también.

Rails 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

Actualización1: sobre el comentario de @JonathanSimmons

Tener que pasar el objeto del usuario dentro del alcance en el modelo de Usuario parece una aproximación hacia atrás

No tiene que pasar el modelo de usuario a este alcance. La instancia de usuario actual se pasa automáticamente a esta lambda. Llámalo así:

user = User.find(9001) user.tasks

Actualización2:

Si es posible, ¿podría ampliar esta respuesta para explicar lo que está sucediendo? Me gustaría entenderlo mejor para poder implementar algo similar. Gracias

Llamar a has_many :tasks en la clase ActiveRecord almacenan una función lambda en alguna variable de clase y es simplemente una forma elegante de generar un método de tasks en su objeto, que llamará a esta lambda. El método generado sería similar al siguiente pseudocódigo:

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