81 votes

Création d'une relation many to many dans Rails

Il s'agit d'un exemple simplifié de ce que j'essaie de réaliser. Je suis relativement nouveau dans Rails et j'ai du mal à comprendre les relations entre les modèles.

J'ai deux modèles, le User et le modèle Category modèle. Un utilisateur peut être associé à de nombreuses catégories. Une catégorie particulière peut apparaître dans la liste des catégories pour de nombreux utilisateurs. Si une catégorie particulière est supprimée, cela doit se refléter dans la liste des catégories d'un utilisateur.

Dans cet exemple :

Mon Categories contient cinq catégories :

\~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| ID | Name                       |
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| 1  | Sports                     | 
| 2  | News                       |
| 3  | Entertainment              |
| 4  | Technology                 |
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Mon Users contient deux utilisateurs :

\~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| ID | Name                       |
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| 1  | UserA                      | 
| 2  | UserB                      |
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

L'utilisateur A peut choisir le sport et la technologie comme catégories.

L'utilisateur B peut choisir Nouvelles, Sports et Divertissement

La catégorie sport est supprimée, les listes de catégories de l'utilisateur A et de l'utilisateur B reflètent cette suppression.

J'ai envisagé de créer un UserCategories qui contient les identifiants d'une catégorie et d'un utilisateur. Cela a fonctionné, j'ai pu consulter les noms des catégories, mais je n'ai pas réussi à faire fonctionner une suppression en cascade et la solution dans son ensemble m'a semblé mauvaise.

Les exemples d'utilisation des fonctions belongs_to et has_many que j'ai trouvés semblent traiter de la mise en correspondance d'une relation univoque. Par exemple, les commentaires sur un article de blog.

  • Comment représenter cette relation de plusieurs à plusieurs en utilisant la fonctionnalité intégrée de Rails ?
  • L'utilisation d'une table séparée entre les deux est-elle une solution viable avec Rails ?

0 votes

En fait, il n'y a rien de mal à utiliser UserCategory il est même souhaitable dans la plupart des cas. Il suffit d'utiliser User.has_many :categories, through: :user_categories . Mais on pourrait peut-être trouver un meilleur nom.

1 votes

Pour les autres, il y a une bonne distribution de rails ici sur le sujet : railscasts.com/episodes/47-deux-many-to-many

177voto

coreyward Points 26109

Vous voulez un has_and_belongs_to_many relation. Le guide décrit très bien comment cela fonctionne avec des graphiques et tout le reste :

http://guides.rubyonrails.org/association_basics.html#the-has-and-belongs-to-many-association

Vous vous retrouverez avec quelque chose comme ça :

# app/models/category.rb
class Category < ActiveRecord::Base
  has_and_belongs_to_many :users
end

# app/models/user.rb
class User < ActiveRecord::Base
  has_and_belongs_to_many :categories
end

Vous devez maintenant créer une table de jonction que Rails pourra utiliser. Rails ne le fera pas automatiquement pour vous. Il s'agit en fait d'une table contenant une référence à chacune des catégories et à chacun des utilisateurs, sans clé primaire.

Générez une migration à partir du CLI comme ceci :

bin/rails g migration CreateCategoriesUsersJoinTable

Ensuite, ouvrez-le et modifiez-le pour qu'il corresponde :

Pour Rails 4.0.2+ (y compris Rails 5.2) :

def change
  # This is enough; you don't need to worry about order
  create_join_table :categories, :users

  # If you want to add an index for faster querying through this join:
  create_join_table :categories, :users do |t|
    t.index :category_id
    t.index :user_id
  end
end

Rails < 4.0.2 :

def self.up
  # Model names in alphabetical order (e.g. a_b)
  create_table :categories_users, :id => false do |t|
    t.integer :category_id
    t.integer :user_id
  end

  add_index :categories_users, [:category_id, :user_id]
end

def self.down
  drop_table :categories_users
end

Une fois ces éléments en place, exécutez vos migrations et vous pourrez connecter les catégories et les utilisateurs avec tous les accesseurs pratiques auxquels vous êtes habitués :

User.categories  #=> [<Category @name="Sports">, ...]
Category.users   #=> [<User @name="UserA">, ...]
User.categories.empty?

10 votes

Un conseil supplémentaire : créez un index composite pour la table. Plus d'informations ici stackoverflow.com/a/4381282/14540

1 votes

@kolrie Bon point ; j'ai ajouté à l'exemple ici par souci d'exhaustivité. À la vôtre !

4 votes

Question bête, cela signifie-t-il qu'il est nécessaire de créer un nouveau modèle ?

12voto

Darlan Dieterich Points 168

Le plus populaire est l'association monotransitive, vous pouvez le faire :

class Book < ApplicationRecord
  has_many :book_authors
  has_many :authors, through: :book_authors
end

# in between
class BookAuthor < ApplicationRecord
  belongs_to :book
  belongs_to :author
end

class Author < ApplicationRecord
  has_many :book_authors
  has_many :books, through: :book_authors
end

Une association has_many :through est souvent utilisée pour configurer une connexion many-to-many. avec un autre modèle. Cette association indique que le modèle modèle déclarant peut être mis en correspondance avec zéro ou plusieurs instances d'un autre modèle en passant par un troisième modèle. Par exemple, considérons un cabinet médical où les patients prennent rendez-vous avec des médecins. Réf : https://guides.rubyonrails.org/association_basics.html#the-has-many-through-association

4voto

B-M Points 991

Je ne fais que compléter la réponse de Coreyward ci-dessus : Si vous avez déjà un modèle qui a un belongs_to , has_many et vous voulez créer une nouvelle relation has_and_belongs_to_many en utilisant la même table, vous devrez :

rails g migration CreateJoinTableUsersCategories users categories

Ensuite,

rake db:migrate

Après cela, vous devrez définir vos relations :

User.rb :

class Region < ApplicationRecord
  has_and_belongs_to_many :categories
end

Catégorie.rb

class Facility < ApplicationRecord
  has_and_belongs_to_many :users
end

Afin de remplir la nouvelle table de jointure avec les anciennes données, vous devrez dans votre console :

User.all.find_each do |u|
  Category.where(user_id: u.id).find_each do |c|
    u.categories <<  c
  end
end

Vous pouvez soit laisser le user_id et category_id des tables Catégorie et Utilisateur ou créez une migration pour la supprimer.

2voto

Jack Ratner Points 21

Si vous voulez ajouter des données supplémentaires sur la relation le has_many :things, through: :join_table peut être ce que vous recherchez. Souvent, cependant, vous n'aurez pas besoin de métadonnées supplémentaires (comme un rôle) sur une relation de jointure, auquel cas l'option has_and_belongs_to_many est certainement la manière la plus simple de procéder (comme dans la réponse acceptée pour ce post SO).

Cependant, disons que vous construisez un site de forums où vous avez plusieurs forums et que vous devez prendre en charge les utilisateurs ayant des rôles différents dans chaque forum auquel ils participent. Il pourrait être utile de permettre le suivi de la façon dont un utilisateur est lié à un forum sur le lien lui-même :

class Forum
  has_many :forum_users
  has_many :users, through: :forum_users

  has_many :admin_forum_users, -> { administrating }, class_name: "ForumUser"
  has_many :viewer_forum_users, -> { viewing }, class_name: "ForumUser"

  has_many :admins, through: :admin_forum_users, source: :user
  has_many :viewers, through: :viewer_forum_users, source: :user
end

class User
  has_many :forum_users
  has_many :forums, through: :forum_users
end

class ForumUser
  belongs_to :user
  belongs_to :forum

  validates :role, inclusion: { in: ['admin', 'viewer'] }

  scope :administrating, -> { where(role: "admin") }
  scope :viewing, -> { where(role: "viewer") }
end

Et votre migration ressemblerait à ceci

class AddForumUsers < ActiveRecord::Migration[6.0]
  create_table :forum_users do |t|
      t.references :forum
      t.references :user

      t.string :role

      t.timestamps
  end
end

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X