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