Erstellen einer Beziehung von vielen zu vielen in Rails

73

Dies ist ein vereinfachtes Beispiel für das, was ich erreichen möchte. Ich bin relativ neu bei Rails und habe Mühe, mich mit den Beziehungen zwischen Modellen auseinanderzusetzen.

Ich habe zwei Modelle, das UserModell und das CategoryModell. Ein Benutzer kann vielen Kategorien zugeordnet werden. Eine bestimmte Kategorie kann für viele Benutzer in der Kategorieliste angezeigt werden. Wenn eine bestimmte Kategorie gelöscht wird, sollte dies in der Kategorieliste für einen Benutzer angezeigt werden.

In diesem Beispiel:

Meine CategoriesTabelle enthält fünf Kategorien:

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| ID | Name |
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| 1 | Sport |
| 2 | Nachrichten |
| 3 | Unterhaltung |
| 4 | Technologie |
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Meine UsersTabelle enthält zwei Benutzer:

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

UserA kann Sport und Technologie als seine Kategorien auswählen

Benutzer B kann Nachrichten, Sport und Unterhaltung wählen

Die Sportkategorie wird gelöscht. Sowohl UserA- als auch UserB-Kategorielisten spiegeln die Löschung wider

Ich habe damit herumgespielt, eine UserCategoriesTabelle zu erstellen , die die IDs einer Kategorie und eines Benutzers enthält. Diese Art von funktionierte, ich konnte die Kategorienamen nachschlagen, aber ich konnte kein kaskadierendes Löschen zum Laufen bringen und die ganze Lösung schien einfach falsch zu sein.

Die Beispiele für die Verwendung der Funktionen "Gehorsam" und "Habe_Viele", die ich gefunden habe, scheinen die Zuordnung einer Eins-zu-Eins-Beziehung zu erörtern. Zum Beispiel Kommentare zu einem Blog-Beitrag.

  • Wie stellen Sie diese Viele-zu-Viele-Beziehung mithilfe der integrierten Rails-Funktionalität dar?
  • Ist die Verwendung einer separaten Tabelle zwischen beiden eine praktikable Lösung bei der Verwendung von Rails?
Fletcher
quelle
Eigentlich ist nichts falsch an der Verwendung des UserCategoryModells, es ist in den meisten Fällen sogar wünschenswert. Sie müssen nur verwenden User.has_many :categories, through: :user_categories. Vielleicht könnte ein besserer Name gefunden werden.
RocketR
1
Für andere gibt es hier eine gute Schiene zu diesem Thema: railscasts.com/episodes/47-two-many-to-many
Dofs

Antworten:

167

Du willst eine has_and_belongs_to_manyBeziehung. Der Leitfaden beschreibt hervorragend, wie dies mit Diagrammen und allem funktioniert:

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

Sie werden am Ende so etwas haben:

# 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

Jetzt müssen Sie eine Join-Tabelle erstellen, die Rails verwenden kann. Rails erledigt dies nicht automatisch für Sie. Dies ist effektiv eine Tabelle mit einem Verweis auf jede der Kategorien und Benutzer und ohne Primärschlüssel.

Generieren Sie eine Migration von der CLI wie folgt:

bin/rails g migration CreateCategoriesUsersJoinTable

Öffnen Sie es dann und bearbeiten Sie es entsprechend:

Für Rails 4.0.2+ (einschließlich 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

Schienen <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

Führen Sie Ihre Migrationen aus, und Sie können Kategorien und Benutzer mit allen praktischen Accessoren verbinden, die Sie gewohnt sind:

User.categories  #=> [<Category @name="Sports">, ...]
Category.users   #=> [<User @name="UserA">, ...]
User.categories.empty?
Coreyward
quelle
9
Ein zusätzlicher Tipp: Erstellen Sie einen zusammengesetzten Index für die Tabelle. Mehr hier stackoverflow.com/a/4381282/14540
kolrie
1
@kolrie Großartiger Punkt; Ich habe das Beispiel hier hinzugefügt, um gründlich zu sein. Prost!
Coreyward
4
dumme Frage, bedeutet das, dass es notwendig ist, ein neues Modell zu erstellen?
TVIEira
1
has_and_belongs_to_many benötigt / erstellt kein Modell. Wenn Sie direkt mit der Beziehung arbeiten oder der Beziehung Attribute hinzufügen müssen, sollten Sie die Option has_many: through verwenden. Folgen Sie dem Link in der Antwort für detaillierte Informationen.
Chong Yu
14
Eine andere Sache, die mir aufgefallen ist, ist die Namenskonvention der Verbindungstabelle. Es muss alphabetisch geordnet sein. Im obigen Beispiel muss der Tabellenname category_users und nicht users_categories sein. "c" vor "u" in diesem Fall.
Chong Yu
10

Am beliebtesten ist 'Mono-transitive Association'. Sie können dies tun:

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

Ein has_many: wird durch Zuordnung häufig verwendet, um eine Viele-zu-Viele-Verbindung mit einem anderen Modell herzustellen. Diese Zuordnung gibt an, dass das deklarierende Modell mit null oder mehr Instanzen eines anderen Modells abgeglichen werden kann, indem ein drittes Modell durchlaufen wird. Stellen Sie sich zum Beispiel eine Arztpraxis vor, in der Patienten Termine beim Arzt vereinbaren. Ref.: Https://guides.rubyonrails.org/association_basics.html#the-has-many-through-association

Darlan D.
quelle
4

Nur zur Ergänzung Antwort des coreyward oben: Wenn Sie bereits ein Modell, das eine hat belongs_to, has_manyBeziehung und Sie wollen eine neue Beziehung erstellen has_and_belongs_to_manymit der gleichen Tabelle , die Sie benötigen:

rails g migration CreateJoinTableUsersCategories users categories

Dann,

rake db:migrate

Danach müssen Sie Ihre Beziehungen definieren:

User.rb:

class Region < ApplicationRecord
  has_and_belongs_to_many :categories
end

Category.rb

class Facility < ApplicationRecord
  has_and_belongs_to_many :users
end

Um die neue Join-Tabelle mit den alten Daten zu füllen, müssen Sie in Ihrer Konsole Folgendes tun:

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

Sie können entweder die user_idSpalte und die category_idSpalte aus den Kategorien- und Benutzertabellen verlassen oder eine Migration erstellen, um sie zu löschen.

BM
quelle
0

Wenn Sie zusätzliche Daten zur Beziehung hinzufügen möchten , ist dies has_many :things, through: :join_tablemöglicherweise das, wonach Sie suchen. Oft müssen Sie jedoch keine zusätzlichen Metadaten (wie eine Rolle) für eine Join-Beziehung benötigen. In diesem Fall has_and_belongs_to_manyist dies definitiv der einfachste Weg (wie in der akzeptierten Antwort für diesen SO-Beitrag).

Angenommen, Sie erstellen eine Forum-Site, auf der Sie mehrere Foren haben und Benutzer unterstützen müssen, die in jedem Forum, an dem sie teilnehmen, unterschiedliche Rollen innehaben. Es kann hilfreich sein, zu verfolgen, wie ein Benutzer mit einem Forum im Forum in Beziehung steht mach mit:

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

Und Ihre Migration würde ungefähr so ​​aussehen

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
Jack Ratner
quelle