¿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_postsen esta situación, así que decidí tomar post_connectionsen su lugar.
Aquí es muy importante :id => falseomitir la idcolumna 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 postsasociación creará registros en la post_connectionstabla según corresponda.
Algunas cosas a tener en cuenta:
a.posts = [b, c], la salida de b.postsno incluye la primera publicación.PostConnection. Normalmente no usa modelos para una has_and_belongs_to_manyasociació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 categorycampo 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 idcolumna. (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 PostConnectionmodelo, 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_ao post_bque estamos tratando con una publicación aquí. Tenemos que contarlo explícitamente.
Ahora el Postmodelo:
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_manyasociación, nos dice el modelo para unirse post_connectionsa 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_basociación de PostConnection.
Solo falta una cosa más , y es que debemos decirle a Rails que a PostConnectiondepende de las publicaciones a las que pertenece. Si uno o ambos de post_a_idy post_b_idfueran NULL, entonces esa conexión no nos diría mucho, ¿verdad? Así es como lo hacemos en nuestro Postmodelo:
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_connectionstiene un extra :dependentde 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_manyasociació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_nameaquí, 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_connectionsy reverse_post_connections; se reflejará claramente en la postsasociació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_manyasociaciones 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_manyy 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 ORlas condiciones anteriores con post_a_idy al post_b_idrevé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_manycomo :finder_sql, :delete_sql, etc. No es bonito. (Estoy abierto a sugerencias aquí también. ¿Alguien?)
:foreign_keyon thehas_many :throughno es necesario, y agregué una explicación sobre cómo usar el:dependentpará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_followsy:followee_followsen user.rb. Para usar una asociación común y corriente (sin bucle) como ejemplo, un equipo puede tener muchas: aplayerstravés:contracts. Esto no es diferente para un jugador , que puede tener muchos:teamsa través:contractstambié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_followsy:followee_followsfueron creados para evitar tal colisión de nombres. Ahora, un usuario puede tener muchas:followersa través:follower_followsy muchos:followeesa través:followee_follows.Para determinar los seguidores de un Usuario (después de una
@user.followeesllamada 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.followersllamada 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_postsdeberí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_nameconfigurado 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
:PostConnectioncon"PostConnection", por supuesto, sustituyendo su nombre de clase.fuente