¿Cómo puedo establecer una relación de varios a varios con el mismo modelo en rieles?
Por ejemplo, cada publicación está conectada a muchas publicaciones.
fuente
¿Cómo puedo establecer una relación de varios a varios con el mismo modelo en rieles?
Por ejemplo, cada publicación está conectada a muchas publicaciones.
Hay varios tipos de relaciones de muchos a muchos; tienes que hacerte las siguientes preguntas:
Eso deja cuatro posibilidades diferentes. Caminaré sobre estos a continuación.
Para referencia: la documentación de Rails sobre el tema . Hay una sección llamada "Muchos a muchos" y, por supuesto, la documentación sobre los métodos de clase en sí.
Este es el código más compacto.
Comenzaré con este esquema básico para sus publicaciones:
create_table "posts", :force => true do |t|
t.string "name", :null => false
end
Para cualquier relación de varios a varios, necesita una tabla de combinación. Aquí está el esquema para eso:
create_table "post_connections", :force => true, :id => false do |t|
t.integer "post_a_id", :null => false
t.integer "post_b_id", :null => false
end
Por defecto, Rails llamará a esta tabla una combinación de los nombres de las dos tablas que estamos uniendo. Pero eso resultaría como posts_posts
en esta situación, así que decidí tomar post_connections
en su lugar.
Aquí es muy importante :id => false
omitir la id
columna predeterminada . Rails quiere esa columna en todas partes excepto en tablas de unión para has_and_belongs_to_many
. Se quejará en voz alta.
Por último, observe que los nombres de las columnas tampoco son estándar (no post_id
) para evitar conflictos.
Ahora, en su modelo, simplemente necesita decirle a Rails sobre estas dos cosas no estándar. Se verá de la siguiente manera:
class Post < ActiveRecord::Base
has_and_belongs_to_many(:posts,
:join_table => "post_connections",
:foreign_key => "post_a_id",
:association_foreign_key => "post_b_id")
end
¡Y eso simplemente debería funcionar! Aquí hay un ejemplo de la sesión de irb ejecutada script/console
:
>> a = Post.create :name => 'First post!'
=> #<Post id: 1, name: "First post!">
>> b = Post.create :name => 'Second post?'
=> #<Post id: 2, name: "Second post?">
>> c = Post.create :name => 'Definitely the third post.'
=> #<Post id: 3, name: "Definitely the third post.">
>> a.posts = [b, c]
=> [#<Post id: 2, name: "Second post?">, #<Post id: 3, name: "Definitely the third post.">]
>> b.posts
=> []
>> b.posts = [a]
=> [#<Post id: 1, name: "First post!">]
Descubrirá que la asignación a la posts
asociación creará registros en la post_connections
tabla según corresponda.
Algunas cosas a tener en cuenta:
a.posts = [b, c]
, la salida de b.posts
no incluye la primera publicación.PostConnection
. Normalmente no usa modelos para una has_and_belongs_to_many
asociación. Por este motivo, no podrá acceder a ningún campo adicional.Bien, ahora ... Tienes un usuario habitual que hoy ha publicado una publicación en tu sitio sobre lo deliciosas que son las anguilas. Este completo extraño llega a su sitio, se registra y escribe una publicación de regaño sobre la ineptitud del usuario habitual. ¡Después de todo, las anguilas son una especie en peligro de extinción!
Entonces, le gustaría dejar en claro en su base de datos que la publicación B es una perorata de regaño sobre la publicación A. Para hacer eso, desea agregar un category
campo a la asociación.
Lo que necesitamos ya no es una has_and_belongs_to_many
, sino una combinación de has_many
, belongs_to
, has_many ..., :through => ...
y un modelo adicional para la tabla de unión. Este modelo adicional es lo que nos da el poder de agregar información adicional a la propia asociación.
Aquí hay otro esquema, muy similar al anterior:
create_table "posts", :force => true do |t|
t.string "name", :null => false
end
create_table "post_connections", :force => true do |t|
t.integer "post_a_id", :null => false
t.integer "post_b_id", :null => false
t.string "category"
end
Observe cómo, en esta situación, post_connections
sí tiene una id
columna. (No hay ningún :id => false
parámetro). Esto es obligatorio, porque habrá un modelo de ActiveRecord regular para acceder a la tabla.
Comenzaré con el PostConnection
modelo, porque es muy simple:
class PostConnection < ActiveRecord::Base
belongs_to :post_a, :class_name => :Post
belongs_to :post_b, :class_name => :Post
end
Lo único que está sucediendo aquí es :class_name
, lo cual es necesario, porque Rails no puede inferir post_a
o post_b
que estamos tratando con una publicación aquí. Tenemos que contarlo explícitamente.
Ahora el Post
modelo:
class Post < ActiveRecord::Base
has_many :post_connections, :foreign_key => :post_a_id
has_many :posts, :through => :post_connections, :source => :post_b
end
Con la primera has_many
asociación, nos dice el modelo para unirse post_connections
a posts.id = post_connections.post_a_id
.
Con la segunda asociación, le estamos diciendo a Rails que podemos llegar a los otros puestos, los conectados a este, a través de nuestra primera asociación post_connections
, seguida de la post_b
asociación de PostConnection
.
Solo falta una cosa más , y es que debemos decirle a Rails que a PostConnection
depende de las publicaciones a las que pertenece. Si uno o ambos de post_a_id
y post_b_id
fueran NULL
, entonces esa conexión no nos diría mucho, ¿verdad? Así es como lo hacemos en nuestro Post
modelo:
class Post < ActiveRecord::Base
has_many(:post_connections, :foreign_key => :post_a_id, :dependent => :destroy)
has_many(:reverse_post_connections, :class_name => :PostConnection,
:foreign_key => :post_b_id, :dependent => :destroy)
has_many :posts, :through => :post_connections, :source => :post_b
end
Además del ligero cambio en la sintaxis, dos cosas reales son diferentes aquí:
has_many :post_connections
tiene un extra :dependent
de parámetros. Con el valor :destroy
, le decimos a Rails que, una vez desaparezca esta publicación, puede seguir adelante y destruir estos objetos. Un valor alternativo que puede usar aquí es :delete_all
, que es más rápido, pero no llamará a ningún gancho de destrucción si los está usando.has_many
asociación para las conexiones inversas , las que nos han vinculado post_b_id
. De esta manera, Rails también puede destruirlos. Tenga en cuenta que tenemos que especificar :class_name
aquí, porque el nombre de la clase del modelo ya no se puede inferir :reverse_post_connections
.Con esto en su lugar, les traigo otra sesión de irb a través de script/console
:
>> a = Post.create :name => 'Eels are delicious!'
=> #<Post id: 16, name: "Eels are delicious!">
>> b = Post.create :name => 'You insensitive cloth!'
=> #<Post id: 17, name: "You insensitive cloth!">
>> b.posts = [a]
=> [#<Post id: 16, name: "Eels are delicious!">]
>> b.post_connections
=> [#<PostConnection id: 3, post_a_id: 17, post_b_id: 16, category: nil>]
>> connection = b.post_connections[0]
=> #<PostConnection id: 3, post_a_id: 17, post_b_id: 16, category: nil>
>> connection.category = "scolding"
=> "scolding"
>> connection.save!
=> true
En lugar de crear la asociación y luego configurar la categoría por separado, también puede crear una PostConnection y terminar con ella:
>> b.posts = []
=> []
>> PostConnection.create(
?> :post_a => b, :post_b => a,
?> :category => "scolding"
>> )
=> #<PostConnection id: 5, post_a_id: 17, post_b_id: 16, category: "scolding">
>> b.posts(true) # 'true' means force a reload
=> [#<Post id: 16, name: "Eels are delicious!">]
Y también podemos manipular las asociaciones post_connections
y reverse_post_connections
; se reflejará claramente en la posts
asociación:
>> a.reverse_post_connections
=> #<PostConnection id: 5, post_a_id: 17, post_b_id: 16, category: "scolding">
>> a.reverse_post_connections = []
=> []
>> b.posts(true) # 'true' means force a reload
=> []
En has_and_belongs_to_many
asociaciones normales , la asociación se define en ambos modelos implicados. Y la asociación es bidireccional.
Pero solo hay un modelo Post en este caso. Y la asociación solo se especifica una vez. Precisamente por eso, en este caso específico, las asociaciones son unidireccionales.
Lo mismo es cierto para el método alternativo con has_many
y un modelo para la tabla de combinación.
Esto se ve mejor cuando simplemente se accede a las asociaciones desde irb y se observa el SQL que genera Rails en el archivo de registro. Encontrarás algo como lo siguiente:
SELECT * FROM "posts"
INNER JOIN "post_connections" ON "posts".id = "post_connections".post_b_id
WHERE ("post_connections".post_a_id = 1 )
Para hacer que la asociación sea bidireccional, tendríamos que encontrar una manera de hacer que Rails tenga OR
las condiciones anteriores con post_a_id
y al post_b_id
revés, para que mire en ambas direcciones.
Desafortunadamente, la única forma de hacer esto que conozco es bastante hacky. Vas a tener que especificar manualmente el SQL usando opciones para has_and_belongs_to_many
como :finder_sql
, :delete_sql
, etc. No es bonito. (Estoy abierto a sugerencias aquí también. ¿Alguien?)
:foreign_key
on thehas_many :through
no es necesario, y agregué una explicación sobre cómo usar el:dependent
parámetro muy útil parahas_many
.Para responder a la pregunta planteada por Shteef:
Asociaciones en bucle bidireccional
La relación seguidor-seguidor entre los usuarios es un buen ejemplo de una asociación en bucle bidireccional. Un usuario puede tener muchos:
Así es como se vería el código de user.rb :
Así es como se muestra el código de follow.rb :
Lo más importante a tener en cuenta son probablemente los términos
:follower_follows
y:followee_follows
en user.rb. Para usar una asociación común y corriente (sin bucle) como ejemplo, un equipo puede tener muchas: aplayers
través:contracts
. Esto no es diferente para un jugador , que puede tener muchos:teams
a través:contracts
también (en el transcurso de tales jugador profesional 's). Pero en este caso, donde solo existe un modelo con nombre (es decir, un Usuario ), nombrar la relación a través: de manera idéntica (pthrough: :follow
. Ej. , O, como se hizo anteriormente en el ejemplo de publicaciones,through: :post_connections
) resultaría en una colisión de nombres para diferentes casos de uso de ( o puntos de acceso a) la tabla de combinación.:follower_follows
y:followee_follows
fueron creados para evitar tal colisión de nombres. Ahora, un usuario puede tener muchas:followers
a través:follower_follows
y muchos:followees
a través:followee_follows
.Para determinar los seguidores de un Usuario (después de una
@user.followees
llamada a la base de datos), Rails ahora puede mirar cada instancia de class_name: "Seguir" donde dicho Usuario es el seguidor (es decirforeign_key: :follower_id
) a través de: such User 's: followee_follows. Para determinar un los seguidores de Usuario (tras una@user.followers
llamada a la base de datos), Rails ahora puede mirar cada instancia de class_name: "Seguir" donde dicho Usuario es el siguiente (es decirforeign_key: :followee_id
) a través de: such User 's: follower_follows.fuente
Si alguien vino aquí para tratar de averiguar cómo crear relaciones de amistad en Rails, entonces lo recomendaría a lo que finalmente decidí usar, que es copiar lo que hizo 'Community Engine'.
Puede consultar:
https://github.com/bborn/communityengine/blob/master/app/models/friendship.rb
y
https://github.com/bborn/communityengine/blob/master/app/models/user.rb
para más información.
TL; DR
..
fuente
Inspirado por @ Stéphan Kochen, esto podría funcionar para asociaciones bidireccionales
entonces
post.posts
&&post.reversed_posts
debería funcionar, al menos funcionó para mí.fuente
Para bidireccional
belongs_to_and_has_many
, consulte la excelente respuesta ya publicada y luego cree otra asociación con un nombre diferente, las claves externas invertidas y asegúrese de que haclass_name
configurado para apuntar al modelo correcto. Salud.fuente
Si alguien tuvo problemas para lograr que la excelente respuesta funcionara, como:
o
Entonces la solución es reemplazar
:PostConnection
con"PostConnection"
, por supuesto, sustituyendo su nombre de clase.fuente