¿Relación de muchos a muchos con el mismo modelo en Rails?

¿Cómo puedo hacer una relación de muchos a muchos con el mismo modelo en Rails?

Por ejemplo, cada publicación está conectada a muchas publicaciones.

Hay varios tipos de relaciones de muchos a muchos; debes hacerte las siguientes preguntas:

  • ¿Quiero almacenar información adicional con la asociación? (Campos adicionales en la tabla de unión)
  • ¿Las asociaciones necesitan ser implícitamente bidireccionales? (Si la publicación A está conectada a la publicación B, la publicación B también está conectada a la publicación A).

Eso deja cuatro posibilidades diferentes. Voy a 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í.

Escenario más simple, unidireccional, sin campos adicionales

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 muchos a muchos, necesita una tabla de unió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 

De forma predeterminada, Rails llamará a esta tabla una combinación de los nombres de las dos tablas a las que nos estamos uniendo. Pero eso se convertiría en posts_posts en esta situación, así que decidí tomar post_connections en post_connections lugar.

Aquí es muy importante :id => false , para omitir la columna de id predeterminada. Rails quiere esa columna en todas partes excepto en las tablas de has_and_belongs_to_many para has_and_belongs_to_many . Se quejará en voz alta.

Finalmente, observe que los nombres de las columnas no son estándares también (no post_id ), para evitar conflictos.

Ahora en su modelo, simplemente necesita decirle a Rails acerca de 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 debería funcionar! Aquí hay una sesión de IRB de ejemplo ejecutada a través de script/console :

 >> a = Post.create :name => 'First post!' => # >> b = Post.create :name => 'Second post?' => # >> c = Post.create :name => 'Definitely the third post.' => # >> a.posts = [b, c] => [#, #] >> b.posts => [] >> b.posts = [a] => [#] 

Descubrirá que la asignación a la asociación de posts creará registros en la tabla post_connections según corresponda.

Algunas cosas a tener en cuenta:

  • Puede ver en la sesión de IRB anterior que la asociación es unidireccional, porque después de a.posts = [b, c] , la salida de b.posts no incluye la primera publicación.
  • Otra cosa que puedes haber notado es que no hay un modelo de PostConnection . Normalmente no utiliza modelos para una asociación has_and_belongs_to_many . Por este motivo, no podrá acceder a ningún campo adicional.

Unidireccional, con campos adicionales

Ahora mismo … Tienes un usuario habitual que hoy ha publicado en tu sitio cómo las anguilas son deliciosas. Este completo desconocido se acerca a su sitio, se registra y escribe una publicación sobre la ineptitud del usuario habitual. ¡Después de todo, las anguilas son una especie en peligro de extinción!

Por lo tanto, le gustaría dejar en claro en su base de datos que la publicación B es una queja de regaño en la publicación A. Para hacer eso, desea agregar un campo de category a la asociación.

Lo que necesitamos ya no es un 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 asociación en sí.

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 tiene una columna de id . (No hay :id => false parámetro :id => false .) Esto es obligatorio, porque habrá un modelo regular de ActiveRecord para acceder a la tabla.

Comenzaré con el modelo PostConnection , 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 sucede aquí es :class_name , que es necesario, porque Rails no puede inferir de post_a o post_b que estamos tratando con una publicación aquí. Tenemos que decirlo explícitamente.

Ahora el modelo de Post :

 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 asociación has_many , le decimos al modelo que se una a las post_connections en posts.id = post_connections.post_a_id .

Con la segunda asociación, le estamos diciendo a Rails que podemos llegar a las otras publicaciones, las conectadas a esta, a través de nuestra primera asociación post_connections , seguida de la asociación PostConnection de PostConnection .

Solo falta una cosa más , y es que tenemos que decirle a Rails que una PostConnection depende de las publicaciones a las que pertenece. Si uno o ambos 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 modelo de Post :

 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 syntax, dos cosas reales son diferentes aquí:

  • El has_many :post_connections tiene un parámetro adicional :dependent . Con el valor :destroy , le decimos a Rails que, una vez que esta publicación desaparece, 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 :delete_all destrucción si los está usando.
  • También hemos agregado una asociación has_many para las conexiones inversas , las que nos han vinculado a través de post_b_id . De esta forma, Rails también puede destruirlos limpiamente. Tenga en cuenta que tenemos que especificar :class_name aquí, porque el nombre de clase del modelo ya no se puede deducir de :reverse_post_connections .

Con esto en su lugar, les traigo otra sesión de IRB a través del script/console :

 >> a = Post.create :name => 'Eels are delicious!' => # >> b = Post.create :name => 'You insensitive cloth!' => # >> b.posts = [a] => [#] >> b.post_connections => [#] >> connection = b.post_connections[0] => # >> 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 terminarla:

 >> b.posts = [] => [] >> PostConnection.create( ?> :post_a => b, :post_b => a, ?> :category => "scolding" >> ) => # >> b.posts(true) # 'true' means force a reload => [#] 

Y también podemos manipular las asociaciones post_connections y reverse_post_connections ; se reflejará claramente en la asociación de posts :

 >> a.reverse_post_connections => # >> a.reverse_post_connections = [] => [] >> b.posts(true) # 'true' means force a reload => [] 

Asociaciones en bucle bidireccionales

En las asociaciones normales has_and_belongs_to_many , la asociación se define en ambos modelos implicados. Y la asociación es bidireccional.

Pero solo hay un modelo de publicación en este caso. Y la asociación solo se especifica una vez. Es exactamente por eso que 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 unión.

Esto se ve mejor cuando simplemente se accede a las asociaciones desde irb, y se observa el SQL que Rails genera 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 la manera de hacer que Rails OR las condiciones anteriores se post_a_id con post_a_id y post_b_id , por lo que se verán en ambas direcciones.

Desafortunadamente, la única forma de hacer esto que yo sé es bastante hacky. Tendrá que especificar manualmente las opciones de SQL usando has_and_belongs_to_many como :finder_sql :delete_sql , etc. No es bonito. (Estoy abierto a sugerencias aquí también. ¿Alguien?)

Para responder la pregunta planteada por Shteef:

Asociaciones en bucle bidireccionales

La relación seguidor-seguidor entre los usuarios es un buen ejemplo de una asociación en bucle bidireccional. Un usuario puede tener muchos:

  • seguidores en su capacidad de seguidor
  • seguidos en su calidad de seguidor.

Así es como podría verse el código para user.rb :

 class User < ActiveRecord::Base # follower_follows "names" the Follow join table for accessing through the follower association has_many :follower_follows, foreign_key: :followee_id, class_name: "Follow" # source: :follower matches with the belong_to :follower identification in the Follow model has_many :followers, through: :follower_follows, source: :follower # followee_follows "names" the Follow join table for accessing through the followee association has_many :followee_follows, foreign_key: :follower_id, class_name: "Follow" # source: :followee matches with the belong_to :followee identification in the Follow model has_many :followees, through: :followee_follows, source: :followee end 

Así es como el código para follow.rb :

 class Follow < ActiveRecord::Base belongs_to :follower, foreign_key: "follower_id", class_name: "User" belongs_to :followee, foreign_key: "followee_id", class_name: "User" end 

Las cosas más importantes a tener en cuenta son probablemente los términos :follower_follows y :followee_follows en user.rb. Para utilizar una asociación de ejecución de la fábrica (sin bucle) como ejemplo, un Equipo puede tener muchos: players través de :contracts . Esto no es diferente para un Jugador , que puede tener muchos :teams través de :contracts también (en el transcurso de la carrera de dicho Jugador ). Pero en este caso, donde solo existe un modelo con nombre (es decir, un Usuario ), nombrar la relación a través de: de manera idéntica (por ej. A through: :follow o, como se hizo anteriormente en el ejemplo de publicaciones, a through: :post_connections ) resultaría en nombrando la colisión para diferentes casos de uso de (o puntos de acceso) en la tabla de unión. :follower_follows y :followee_follows fueron creados para evitar dicha colisión de nombres. Ahora, un usuario puede tener muchos :followers través de :follower_follows y muchos :followees través de :followee_follows .

Para determinar un usuario : followees (en una llamada @user.followees a la base de datos), Rails ahora puede ver cada instancia de class_name: "Seguir" donde dicho usuario es el seguidor (es decir, foreign_key: :follower_id ) a través de: tales User 's: followee_follows. Para determinar un usuario : seguidores (con una llamada @user.followers a la base de datos), Rails ahora puede ver cada instancia de class_name: "Seguir" donde dicho usuario es el seguidor (es decir, foreign_key: :followee_id ) a través de: tales User 's: follower_follows.

Si alguien viniera aquí para tratar de descubrir cómo crear relaciones de amistad en Rails, les recomendaría lo que finalmente decidí usar, que es copiar lo que hizo ‘Community Engine’.

Puedes referirte a:

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

 # user.rb has_many :friendships, :foreign_key => "user_id", :dependent => :destroy has_many :occurances_as_friend, :class_name => "Friendship", :foreign_key => "friend_id", :dependent => :destroy 

..

 # friendship.rb belongs_to :user belongs_to :friend, :class_name => "User", :foreign_key => "friend_id" 

Para los bidireccionales belongs_to_and_has_many , consulte la gran respuesta ya publicada, y luego cree otra asociación con un nombre diferente, las claves foráneas invertidas y asegúrese de que class_name establecido para señalar el modelo correcto. Aclamaciones.

Si alguien tuvo problemas para obtener la respuesta excelente para trabajar, como por ejemplo:

(El objeto no admite #inspect)
=>

o

NoMethodError: método no definido `split ‘para: Misión: Símbolo

Entonces la solución es reemplazar :PostConnection con "PostConnection" , sustituyendo tu nombre de clase por supuesto.

Inspirado por @ Stéphan Kochen, esto podría funcionar para asociaciones bidireccionales

 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") has_and_belongs_to_many(:reversed_posts, :class_name => Post, :join_table => "post_connections", :foreign_key => "post_b_id", :association_foreign_key => "post_a_id") end 

entonces post.posts && post.reversed_posts deberían funcionar, al menos funcionaron para mí.