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
Por lo que entonces Task.create(owner_id:1, assignee_id: 2)
Esto me permite realizar Task.first.owner
cuál devuelve el usuario uno y Task.first.assignee
cuál devuelve el usuario dos pero User.first.task
no devuelve nada. Lo cual se debe a que la tarea no pertenece a un usuario, pertenece al propietario y al cesionario . Entonces,
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 por completo ya que dos claves externas no parecen ser compatibles.
Entonces, lo que quiero es poder decir User.tasks
y obtener tanto las tareas asignadas como las propias de los usuarios.
Básicamente, construye de alguna manera una relación que equivaldría 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 problema parece estar cerca de lo que quiero: Rails - Asociación de claves de índice múltiple
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 Rails 4
, sigo recibiendo el siguiente error:
ActiveRecord::PreparedStatementInvalid: missing value for :owner_id in :donor_id => :person_id OR assignee_id => :person_id
fuente
Respuestas:
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
enUser
clase.Usar
has_many :tasks
no tiene ningún sentido ya que no tenemos ninguna columna nombradauser_id
en la tablatasks
.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
User.first.assigned_tasks
tambiénUser.first.owned_tasks
.Ahora, puede definir un método llamado
tasks
que devuelva la combinación deassigned_tasks
yowned_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 el
tasks
, se emitirán dos consultas en lugar de una, y luego, el resultado de esas dos consultas también deben unirse.Entonces, para obtener las tareas que pertenecen a un usuario, definiríamos un
tasks
método personalizado enUser
clase de la siguiente manera:def tasks Task.where("owner_id = ? OR assigneed_id = ?", self.id, self.id) end
De esta manera, obtendrá todos los resultados en una sola consulta y no tendremos que fusionar o combinar ningún resultado.
fuente
has_many
"no tiene sentido" Si lee la respuesta aceptada, verá que no terminamos usando unahas_many
declaración en absoluto. Suponiendo que sus requisitos coincidan con las necesidades de todos los demás visitantes es improductivo, esta respuesta podría haber sido menos crítica.owned_tasks
yassigned_tasks
para obtener todas las tareas tendrá una pista de rendimiento. De todos modos, he actualizado mi respuesta y he incluido un método en laUser
clase para obtener todos los asociadostasks
, obtendrá los resultados en una consulta y no es necesario fusionar / combinar.Task
modelo en realidad no son necesarias. Dado que tiene lasbelongs_to
relaciones denominadas:owner
y:assignee
, Rails asumirá de forma predeterminada que las claves externas se denominanowner_id
yassignee_id
, respectivamente.Ampliando 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 para el efecto de
WHERE tasks.user_id = ?
, que falla porque no hay ningunauser_id
columna en este escenario.Descubrí que todavía es posible hacer que funcione con una
has_many
asociación, solo necesita anular el alcance de esta cláusula adicional where agregada por Rails.class User < ApplicationRecord has_many :tasks, ->(user) { unscope(:where).where(owner: user).or(where(assignee: user) } end
fuente
belongs_to
(donde el padre no tiene identificación, por lo que debe basarse en el PK de varias columnas). Simplemente dice que la relación es nula (y mirando la consola, no puedo ver que la consulta lambda se ejecute).Carriles 5:
debe anular el alcance de la cláusula where predeterminada para ver la respuesta de @Dwight si aún desea una asociación has_many.
Aunque
User.joins(:tasks)
me daArgumentError: 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, también puede usar la solución @Arslan Ali.
Carriles 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ón 1: Con respecto al comentario de @JonathanSimmons
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:
Llamar
has_many :tasks
a la clase ActiveRecord almacenará una función lambda en alguna variable de clase y es solo una forma elegante de generar untasks
método 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
fuente
where
consulta en lugar de simplemente usar el lambda anteriorResolví 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
ActiveRecord::Relation
objeto que estaba buscando.Entonces ahora puedo hacer algo como esto:
User.first.tasks.completed
y el resultado es que todas las tareas completadas son propiedad o están asignadas al primer usuario.fuente
Mi respuesta a Asociaciones y (múltiples) claves externas en rieles (3.2): ¡cómo describirlas en el modelo y escribir migraciones es solo para usted!
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 el registro existente, no haga lo que le he sugerido, porque este truco causará problemas cuando haga algo como esto:
La razón es que rails intentará 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 fuera de la caja, pero no haga eso.
fuente
Desde Rails 5 también puede hacer lo que es la forma más segura de ActiveRecord:
def tasks Task.where(owner: self).or(Task.where(assignee: self)) end
fuente