Polymorphic Association

Polymorphic Association

Eine polymorphe Beziehung ermöglicht es einem Modell, zu verschiedenen Typen anderer Modelle zu gehören. Hier zeige ich, wie man ein einzelnes Kommentarmodell dazu bringt, Artikeln, Fotos und Veranstaltungen zugeordnet zu werden.

Folgende Anwendung

Die folgende Anwendung verfügt über drei verschiedene Modelle: Artikel, Fotos und Veranstaltungen.
$ rails new polimorphic_app
$ cd polimorphi_app
Nun erstellen wir die Modelle.
$ rails g scaffold Article name content:text
$ rails g scaffold Photo name filename
$ rails g scaffold Event name starts_at:datetime ends_at:datetime description:text
$ rails db:migrate
Jetzt noch den Basis-Code, erst fügen wir die gems in das Gemfile und dann erstellen wir _main.scss.
// Gemfile

gem "image_processing", "~> 1.2"
gem 'sassc-rails'
Die gems installieren.
$ bundle install
und jetzt ein paar Daten in die Datenbank eingeben. zuvor müssen wir noch in die photo.rb und den photos_controller.rb etwas einfügen.
// app/models/photo.rb

class Photo < ApplicationRecord
  has_one_attached :image
end
und in den controller image frei geben.
// app/controllers/photos_controller.rb

  def photo_params
    params.require(:photo).permit(:name, :filename, :image)
  end
So jetzt haben wir ein Grundgerüsst mit dem wir arbeiten können.

1.png 967 KB


Wir möchten Benutzern die Möglichkeit geben, Kommentare zu allen drei davon hinzuzufügen. Eine Option wäre, für jeden Typ ein separates Kommentarmodell hinzuzufügen, was zu einem Artikelkommentarmodell, einem Fotokommentarmodell und einem Veranstaltungskommentarmodell führen würde. Dies würde jedoch viel Arbeit bedeuten und Duplikation in unserer Anwendung schaffen, insbesondere da die drei Arten von Kommentaren das gleiche Verhalten und die gleichen Attribute haben sollten. In einer solchen Situation sollten wir die Verwendung einer polymorphen Beziehung in Betracht ziehen. In dieser Episode zeigen wir Ihnen, wie Sie dies tun.

Erstellen eines einzelnen Kommentarmodells

Zunächst werden wir ein einzelnes Modell für Kommentare erstellen, das wir Kommentar nennen und dem wir ein Inhaltsfeld geben werden. Um eine polymorphe Beziehung einzurichten, müssen wir uns fragen, was die anderen Modelle, zu denen dieses in Beziehung steht, gemeinsam haben. In diesem Fall sind sie alle kommentierbar, und daher werden wir diesem Modell zwei weitere Felder hinzufügen, die commentable_id und commentable_type genannt werden.
$ rails g model comment content:text commentable_id:integer commentable_type:string
Der Klassenname wird in commentable_type gespeichert, und Rails wird dies zusammen mit commentable_id verwenden, um den Datensatz zu bestimmen, mit dem der Kommentar verknüpft ist. Da diese beiden Spalten oft gemeinsam abgefragt werden, haben wir für sie einen Index hinzugefügt. Eine polymorphe Beziehung kann auf eine andere Weise angegeben werden, indem Sie belongs_to aufrufen, wie folgt:
// db/migrate/20240126034840_create_comments.rb

class CreateComments < ActiveRecord::Migration[7.1]
  def change
    create_table :comments do |t|
      t.text :content
      t.belongs_to :commentable, polymorphic: true

      t.timestamps
    end
    add_index :comments, %i[commentable_id commentable_type]
  end
end
Dies wird die id- und type-Spalten für uns generieren. Wir können nun die neue Tabelle generieren, indem wir rails db:migrate ausführen.
$ rails db:migrate
Als nächstes müssen wir unser Comment-Modell ändern und eine belongs_to-Assoziation für commentable hinzufügen.
// app/models/comment.rb

class Comment < ApplicationRecord
  belongs_to :commentable, polymorphic: true
end
Standardmäßig werden die Felder commentable_id und commentable_type zur Liste der attr_accessible hinzugefügt, aber da wir nicht möchten, dass auf diese Felder über Massenzuweisung zugegriffen werden kann, haben wir sie entfernt. Als nächstes müssen wir in jedes der anderen Modelle gehen und die andere Seite der Beziehung festlegen. Es ist wichtig, die Option as hier zu spezifizieren und sie auf den anderen Namen der Beziehung zu setzen, in diesem Fall commentable.
// app/models/article.rb

class Article < ApplicationRecord
  has_many :comments, as: :commentable
end

// app/models/event.rb

class Event < ApplicationRecord
  has_many :comments, as: :commentable
end

// app/models/photo.rb

class Photo < ApplicationRecord
  has_one_attached :image
  has_many :comments, as: :commentable
end
Wenn der Kommentar erstellt wird, setzt Rails automatisch das Attribut commentable_type auf "Article", damit es weiß, mit welchem Typ von Modell der Kommentar verknüpft ist. Wenn wir den umgekehrten Weg gehen und herausfinden möchten, zu welchem Artikel ein Kommentar gehört, können wir nicht einfach comment.article aufrufen, da dies völlig dynamisch ist. Stattdessen müssen wir commentable aufrufen.

Diese Methode kann auch ein Foto, eine Veranstaltung oder jedes andere Modell zurückgeben, mit dem wir den Kommentar verknüpfen möchten.

Verwendung einer polymorphen Beziehung in unserer Anwendung

Nun, da wir wissen, wie polymorphe Beziehungen funktionieren, wie verwenden wir sie in unserer Anwendung? Insbesondere wie verwenden wir verschachtelte Ressourcen, damit wir einen Pfad wie /articles/1/comments verwenden können, um die Kommentare zu einem bestimmten Artikel zu erhalten? Zuerst erstellen wir einen CommentsController, damit wir einen Ort zum Auflisten von Kommentaren haben. Wir geben ihm auch eine neue Aktion, damit wir Kommentare erstellen können.
$ rails g controller comments index new
Wir möchten, dass Kommentare eine verschachtelte Ressource unter Artikeln, Fotos und Veranstaltungen sind, daher müssen wir unsere Routendatei ändern.
// config/routes.rb

Rails.application.routes.draw do
  resources :photos do
    resources :comments
  end

  resources :events do
    resources :comments
  end

  resources :articles do
    resources :comments
  end
  root to: 'articles#index'

  get 'up' => 'rails/health#show', as: :rails_health_check
end
Diese Routen werden zum generierten CommentsController führen. In der Index-Aktion möchten wir die Kommentare für das jeweilige commentable-Modell abrufen, das übergeben wurde. Um dies zu tun, müssen wir den commentable-Datensatz abrufen, der die Kommentare besitzt, aber vorerst nehmen wir an, dass die übergebene ID zu einem Artikel gehört.
// app/controllers/comments_controller.rb

class CommentsController < ApplicationController
  def index
    @commentable = Article.find(params[:article_id])
    @comments = @commentable.comments
  end

  def new; end

  private

  # Only allow a list of trusted parameters through.
  def event_params
    params.require(:comment).permit(:content)
  end
end
Im View-Template durchlaufen wir die Kommentare und zeigen sie an.
// app/views/comments/index.html.erb

<h1>Comments</h1>

<div id="comments">
  <% @comments.each do |comment| %>
  <div class="comment">
    <%= simple_format comment.content %>
  </div>
  <% end %>
</div>
Wenn wir jetzt /articles/1/comments besuchen, sehen wir den einen Kommentar, den wir zuvor zu diesem Artikel hinzugefügt haben. (Wir haben bereits einige CSS-Stile zur _comments.scss-Datei hinzugefügt.)
// app/assets/stylesheets/_comments.scss

#comments {
  font-size: 12px;
  background-color: #EEE;
  padding: 0 20px;
  width: 325px;
  border: solid 1px #777;
}

.comment {
  border-bottom: solid 1px #999;
  margin: 15px 0;
  &:last-child {
    border-bottom: none;
  }
}

2.png 597 KB


Wenn wir versuchen, die Kommentarseite für ein Foto zu besuchen, funktioniert dies nicht, da der Code versucht, einen Artikel zu finden, obwohl keine article_id übergeben wurde. Um dies zu beheben, müssen wir die Suche im Controller dynamischer gestalten. Wir verschieben den Code zum Finden des zugehörigen Modells in einen before_filter. Wir müssen den Namen der commentable-Ressource und ihre ID bestimmen. Diese erhalten wir von request.path, indem wir es bei jedem Schrägstrich aufteilen und die zweiten und dritten Elemente abrufen. Wenn also der Pfad /photos/1 ist, werden diese beiden Elemente verwendet. Wir können diese verwenden, um @commentable zu setzen, indem wir singlularize.classify.constantize aufrufen, um die Klasse des Modells zu erhalten, und dann find darauf aufrufen, um die Instanz nach der ID zu erhalten.
// app/controllers/comments_controller.rb

class CommentsController < ApplicationController
  before_action :load_commentable

  def index
    @comments = @commentable.comments
  end

  def new; end

  private

  def load_commentable
    resource, id = request.path.split('/')[1, 2]
    @commentable = resource.singularize.classify.constantize.find(id)
  end

  # Only allow a list of trusted parameters through.
  def event_params
    params.require(:comment).permit(:content)
  end
end
Dies ist der einfachste Weg, dies zu tun, führt jedoch zu einer engen Verknüpfung zwischen dem Controller und dem Format der URL. Wenn wir benutzerdefinierte URLs verwenden, könnten wir eine andere Technik verwenden, wie diese:
def load_commentable
  klass = [Article, Photo, Event].detect { |c| params["#{c.name.underscore}_id"]}
  @commentable = klass.find(params["#{klass.name.underscore}_id"])
end
Dies wird jede der commentable-Klassen nehmen und in den Parametern nach einer Übereinstimmung mit dem Klassennamen gefolgt von _id suchen. Dann verwenden wir die übereinstimmende Klasse, um eine Übereinstimmung mit der ID zu finden. Wenn wir jetzt versuchen, die Kommentare für ein Foto anzuzeigen, funktioniert die Seite.

3.png 597 KB

Adding Links

Als nächstes sehen wir uns an, wie wir mit Links umgehen. Wie erstellen wir einen Link zum Hinzufügen eines Kommentars zu einem kommentierbaren Element? Wenn die Kommentarseite nur für Fotos wäre, könnten wir den Pfad new_photo_comment_path verwenden und ein Foto übergeben. Wir müssen jedoch auch Artikel und Veranstaltungen unterstützen, sodass dieser Ansatz hier nicht funktioniert. Was wir tun können, ist, ein Array zu übergeben, sodass Rails den Aufruf dynamisch basierend darauf generiert, was wir übergeben, wie folgt:
// app/views/comments/index.html.erb

<p><%= link_to "New Comment", [:new, @commentable, :comment] %></p>
Rails wird den richtigen Pfad jetzt dynamisch basierend auf dem Typ der Variable @commentable generieren. Wenn wir auf den Link klicken, gelangen wir zur new-Aktion des CommentsControllers. Als nächstes schreiben wir den Code, um das Hinzufügen von Kommentaren zu behandeln. Der Code für die Aktionen new und create ist ziemlich standardmäßig. In new erstellen wir einen Kommentar über die comments-Assoziation, während wir in create, wenn der Kommentar erfolgreich gespeichert wird, zur Index-Aktion umleiten und die Array-Technik verwenden, die wir im View verwendet haben, um zum Artikelkommentarpfad, zum Fotokommentarpfad oder zum Veranstaltungskommentarpfad zu gelangen, abhängig davon, gegen was der neue Kommentar gespeichert wird.
// app/controllers/comments_controller.rb

def new
    @comment = @commentable.comments.new
  end

  def create
    @comment = @commentable.comments.new(event_params)
    if @comment.save
      redirect_to [@commentable, :comments], notice: 'Comment created.'
    else
      render :new
    end
  end
Als nächstes schreiben wir die Ansicht für das Hinzufügen eines Kommentars.
// app/views/comments/new.html.erb

<h1>New Comment</h1>

<%= form_for [@commentable, @comment] do |f| %>
<% if @comment.errors.any? %>
<div class="error_messages">
  <h2>Please correct the following errors.</h2>
  <ul>
    <% @comment.errors.full_messages.each do |msg| %>
    <li><%= msg %></li>
    <% end %>
  </ul>
</div>
<% end %>

<div class="field">
  <%= f.text_area :content, rows: 8 %>
</div>
<div class="actions">
  <%= f.submit %>
</div>
<% end %>
Dieser Code ist ebenfalls ziemlich standardmäßig mit einem Formular zum Bearbeiten des Kommentarinhalts. Ein wesentlicher Unterschied ist das Array, das wir an form_for übergeben, damit es die URL korrekt für die polymorphe Beziehung generiert. Wenn wir die Seite für den neuen Kommentar jetzt neu laden, sehen wir das Formular, und wenn wir einen Kommentar hinzufügen, werden wir zurück zur richtigen Seite umgeleitet.

4.png 616 KB


Aus der Benutzeroberflächensicht wäre es wahrscheinlich besser, wenn alle Kommentarfunktionen inline auf der jeweiligen Show-Seite des Modells wären. Dies ist einfach zu erreichen, wenn wir das, was wir erstellt haben, in ein paar Partialansichten verschieben. Wir werden das Formular von der Seite "neuer Kommentar" in ein Partial unter /app/views/comments/_form.html.erb verschieben und es dann in der neuen Vorlage verwenden.
// app/views/comments/new.html.erb

<h1>New Comment</h1>

<%= render 'form' %>
In der Index-Ansicht erstellen wir ein Partial für den Code, der die Kommentare auflistet.
// app/views/comments/_comment.html.erb

<div id="comments">
  <% @comments.each do |comment| %>
    <div class="comment">
      <%= simple_format comment.content %>
    </div>
  <% end %>
</div>
Das lässt die Index-Vorlage wie folgt aussehen:
// app/views/comments/index.html.erb

<h1>Comments</h1>

<%= render 'comment' %>

<p><%= link_to "New Comment", [:new, @commentable, :comment] %></p>
Nun können wir in der Show-Vorlage für jedes kommentierbare Modell diese Partials hinzufügen.
// app/views/articles/show.html.erb

<%= render @article %>

<div>
  <%= link_to "Edit this article", edit_article_path(@article) %> |
  <%= link_to "Back to articles", articles_path %>

  <%= button_to "Destroy this article", @article, method: :delete %>
</div>


<h2>Comments</h2>

<%= render "comments/comment" %>
<%= render "comments/form" %>
Wir müssen auch die Show-Aktion ändern, um die Instanzvariablen für diese Partials vorzubereiten.
// app/controllers/articles_controller.rb

def show
  @commentable = @article
  @comments = @commentable.comments
  @comment = Comment.new
end
Alternativ könnten wir einige Umbenennungen vornehmen und die Partials so ändern, dass wir nicht alle diese Instanzvariablen angeben müssen. Unabhängig von der gewählten Vorgehensweise müssen wir dieselben Änderungen auch an den Controllern und Ansichten für Fotos und Veranstaltungen vornehmen.

Schließlich müssen wir im CommentsController das Umleitungsverhalten ändern, damit der Benutzer nach erfolgreicher Erstellung eines Kommentars zur entsprechenden Show-Aktion des kommentierbaren Elements umgeleitet wird, anstatt zur Index-Aktion.
// app/controllers/comments_controller.rb

def create
  @comment = @commentable.comments.new(event_params)
  if @comment.save
    redirect_to @commentable, notice: "Comment created."
  else
    render :new
  end
end
Lass es uns ausprobieren. Wenn wir jetzt einen Artikel besuchen, sehen wir seine Kommentare zusammen mit dem Formular zum Erstellen eines neuen Kommentars. Wenn wir einen Kommentar hinzufügen, werden wir zur gleichen Seite zurückgeleitet, auf der der neue Kommentar angezeigt wird.

5.png 711 KB


Wir können diese Schritte problemlos auf Fotos und Veranstaltungen anwenden, um eine Inline-Kommentarfunktion hinzuzufügen.
Meld dich an und schreibe ein Kommentar