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.
Vorbereitung
Eine Rails-Anwendung mit Artikeln, Fotos und Veranstaltungen erstellen
In diesem Tutorial erstellen wir eine Rails-Anwendung mit drei unterschiedlichen Modellen:
Article für Artikel
Photo für Fotos
Event für Veranstaltungen
Alle drei Modelle sollen später Kommentare erhalten können. Statt für jeden Typ ein eigenes Kommentar-Modell anzulegen, verwenden wir dafür eine polymorphe Assoziation. Dadurch kann ein einziges Comment-Model mit unterschiedlichen Modellen unserer Anwendung verknüpft werden. Zunächst erstellen wir eine neue Rails-Anwendung und wechseln in das Projektverzeichnis:
$ rails new polymorphic_app
$ cd polymorphic_app
Anschließend erzeugen wir unsere drei Ressourcen:
$ 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
Danach führen wir die erzeugten Migrationen aus:
$ rails db:migrate
Damit besitzen wir zunächst drei voneinander unabhängige Ressourcen, die wir über die vom Scaffold erzeugten Controller und Views verwalten können.
Bildverarbeitung vorbereiten
Für unsere Fotos möchten wir zusätzlich Active Storage verwenden. Dafür ergänzen wir das Gem für die Bildverarbeitung in unserer Gemfile:
# Gemfile
gem "image_processing", "~> 1.2"
Anschließend installieren wir die Abhängigkeiten:
$ bundle install
Falls Active Storage in der Anwendung noch nicht eingerichtet wurde, installieren wir außerdem die benötigten Active-Storage-Tabellen:
Damit das Bild über das Formular übergeben werden kann, ergänzen wir :image bei den erlaubten Parametern des PhotosController:
# app/controllers/photos_controller.rbdefphoto_params
params.expect(photo: [:name, :filename, :image])
end
Damit steht unser Grundgerüst: Wir können Artikel, Fotos und Veranstaltungen verwalten und einem Foto über Active Storage ein Bild zuordnen.
Warum eine polymorphe Assoziation?
Als Nächstes möchten wir Benutzern ermöglichen, alle drei Arten von Inhalten zu kommentieren.
Eine Möglichkeit wäre, drei unterschiedliche Kommentar-Modelle anzulegen:
ArticleComment
PhotoComment
EventComment
Diese Modelle würden jedoch weitgehend dieselben Aufgaben übernehmen und dieselben Attribute besitzen. Wir würden damit praktisch dieselbe Kommentarfunktion mehrfach implementieren.
Stattdessen erstellen wir ein einziges Comment-Model und verbinden es polymorph mit unseren verschiedenen Modellen.
Ein Kommentar kann dadurch beispielsweise zu einem Article, einem Photo oder einem Event gehören, ohne dass wir für jeden dieser Typen ein eigenes Kommentar-Modell benötigen.
Im nächsten Schritt richten wir genau diese polymorphe Beziehung ein.
1.webp
241 KB
Ein einzelnes Kommentarmodell erstellen
Eine polymorphe Beziehung für Kommentare einrichten
Wir möchten Kommentare mit Article, Photo und Event verknüpfen, ohne für jedes dieser Modelle ein eigenes Kommentar-Modell erstellen zu müssen.
Dazu benötigen wir eine gemeinsame Bezeichnung für die Modelle, die kommentiert werden können. In unserem Fall nennen wir diese Beziehung:
commentable
Ein Kommentar gehört also zu einem kommentierbaren Objekt. Welches konkrete Model dahintersteht, entscheidet Rails anhand einer polymorphen Assoziation.
Wir können das Comment-Model einschließlich der polymorphen Referenz direkt mit dem Rails-Generator erstellen:
$ rails g model Comment content:text commentable:references{polymorphic}
Rails erzeugt dadurch für die polymorphe Beziehung zwei wichtige Datenbankspalten:
commentable_type
commentable_id
commentable_type speichert den Typ des zugehörigen Models, beispielsweise:
Article
Photo
Event
commentable_id enthält die ID des konkreten Datensatzes.
Ein Kommentar könnte intern beispielsweise folgende Zuordnung besitzen:
commentable_type: "Article"
commentable_id: 1
Rails weiß dadurch, dass dieser Kommentar zum Article mit der ID 1 gehört.
Die Migration
Die vom Generator erzeugte Migration enthält die polymorphe Referenz:
commentable ist dabei keine eigene Modelklasse. Rails ermittelt über commentable_type und commentable_id, mit welchem Datensatz der Kommentar verbunden ist.
Haben wir beispielsweise einen Kommentar in der Variable comment, können wir einfach Folgendes aufrufen:
comment.commentable
Je nach Kommentar kann das Ergebnis ein Article, ein Photo oder ein Event sein.
Wir benötigen deshalb keine Aufrufe wie:
comment.article
comment.photo
comment.event
Die polymorphe Beziehung wird vollständig über commentable aufgelöst.
Die andere Seite der Beziehung einrichten
Jetzt müssen wir noch festlegen, dass unsere drei vorhandenen Models Kommentare besitzen können.
ist hierbei entscheidend. Sie sagt Rails, dass Article, Event beziehungsweise Photo die polymorphe Seite der commentable-Beziehung darstellen.
dependent: :destroy sorgt zusätzlich dafür, dass die zugehörigen Kommentare gelöscht werden, wenn beispielsweise ein Artikel gelöscht wird. Dadurch bleiben keine Kommentare zurück, deren ursprünglicher Inhalt nicht mehr existiert.
Die Beziehung verwenden
Jetzt können wir Kommentare direkt über die jeweilige Assoziation erstellen.
Bei einem Artikel funktioniert das beispielsweise so:
article = Article.first
comment = article.comments.create(content: "Mein erster Kommentar")
Rails setzt dabei die polymorphen Werte automatisch passend zum Artikel.
Wir müssen commentable_type und commentable_id also nicht selbst setzen.
Umgekehrt gelangen wir vom Kommentar wieder zu seinem zugehörigen Objekt:
comment.commentable
Damit haben wir ein einziges Comment-Model, das mit allen drei unterschiedlichen Models unserer Anwendung verwendet werden kann.
Polymorphe Beziehungen in der Anwendung verwenden
Kommentare als verschachtelte Ressource einrichten
Unsere Models sind nun miteinander verbunden. Als Nächstes möchten wir die Kommentare über unsere Anwendung aufrufen und verwalten können.
Alle drei URLs führen zum selben CommentsController. Anhand der übergebenen Parameter ermitteln wir dort, ob die Kommentare zu einem Artikel, einem Foto oder einer Veranstaltung gehören.
Zunächst erstellen wir den Controller:
$ rails g controller Comments index new
Verschachtelte Routes definieren
Kommentare gehören immer zu einem unserer kommentierbaren Objekte. Deshalb richten wir sie als verschachtelte Ressource ein:
enthält params[:article_id] die ID des Artikels. Der Controller lädt entsprechend:
@commentable = Article.find(params[:article_id])
Bei:
/photos/1/comments
wird dagegen das entsprechende Photo geladen.
Anschließend funktioniert der Zugriff auf die Kommentare unabhängig davon, welcher Typ geladen wurde:
@comments = @commentable.comments
Genau darin liegt der Vorteil unserer polymorphen Beziehung: Ab diesem Punkt muss der restliche Controller nicht mehr unterscheiden, ob @commentable ein Article, Photo oder Event ist.
Kommentare anzeigen
Nun erstellen wir die Index-View:
<!-- app/views/comments/index.html.erb --><h1>Comments</h1><divid="comments"><%@comments.each do |comment| %><divclass="comment"><%= simple_format comment.content %></div><%end%></div>
Wenn wir jetzt beispielsweise:
/articles/1/comments
aufrufen, werden alle Kommentare des entsprechenden Artikels angezeigt.
Dasselbe funktioniert auch für:
/photos/1/comments
und:
/events/1/comments
Der CommentsController bleibt dabei derselbe. Lediglich das in @commentable gespeicherte Objekt ändert sich.
2.webp
32.2 KB
Kommentare hinzufügen
Dynamische Links für polymorphe Ressourcen verwenden
Unsere Kommentare können nun zu Artikeln, Fotos und Veranstaltungen gehören. Als Nächstes möchten wir einen Link anbieten, über den ein neuer Kommentar erstellt werden kann.
Bei einer ausschließlich für Fotos gedachten Kommentarfunktion könnten wir einen festen Route Helper verwenden:
<%= link_to "New Comment", new_photo_comment_path(@photo) %>
Unsere Kommentarfunktion soll jedoch mit allen drei Models funktionieren. Wir wissen innerhalb der gemeinsamen Comments-View nicht zwingend, ob @commentable gerade ein Article, Photo oder Event enthält.
Rails unterstützt dafür polymorphe Routen.
In unserer Index-View ergänzen wir:
<!-- app/views/comments/index.html.erb --><h1>Comments</h1><divid="comments"><%@comments.each do |comment| %><divclass="comment"><%= simple_format comment.content %></div><%end%></div><p><%= link_to "New Comment", [:new, @commentable, :comment] %></p>
Das Array:
[:new, @commentable, :comment]
wird von Rails abhängig vom Typ von @commentable in die passende Route umgesetzt.
Enthält @commentable beispielsweise einen Artikel, entspricht das sinngemäß:
new_article_comment_path(@commentable)
Bei einem Foto wird daraus:
new_photo_comment_path(@commentable)
und bei einer Veranstaltung:
new_event_comment_path(@commentable)
Wir benötigen dadurch keine unterschiedlichen Views für unsere drei kommentierbaren Models.
Einen neuen Kommentar vorbereiten
Nun ergänzen wir die new-Action im CommentsController.
Da load_commentable bereits über unseren before_action ausgeführt wurde, können wir den neuen Kommentar direkt über die Assoziation erstellen:
# app/controllers/comments_controller.rbdefnew@comment = @commentable.comments.new
end
Dadurch wird der neue Kommentar bereits dem richtigen @commentable zugeordnet.
Rails erzeugt daraus abhängig vom Typ von @commentable beispielsweise:
/articles/1/comments
oder:
/photos/1/comments
beziehungsweise:
/events/1/comments
Kann der Kommentar nicht gespeichert werden, zeigen wir das Formular erneut an:
render :new, status: :unprocessable_entity
Das Formular erstellen
Jetzt benötigen wir noch die View für einen neuen Kommentar.
In älteren Rails-Anwendungen wurde dafür häufig form_for verwendet. In unserer aktuellen Anwendung verwenden wir stattdessen form_with.
Auch hier können wir Rails die richtige verschachtelte Route anhand unserer beiden Objekte bestimmen lassen:
<!-- app/views/comments/new.html.erb --><h1>New Comment</h1><%= form_with model: [@commentable, @comment] do |form| %><%if@comment.errors.any? %><divclass="error_messages"><h2>Please correct the following errors.</h2><ul><%@comment.errors.full_messages.each do |message| %><li><%= message %></li><%end%></ul></div><%end%><divclass="field"><%= form.text_area :content, rows: 8%></div><divclass="actions"><%= form.submit %></div><%end%>
Entscheidend ist auch hier das Array:
[@commentable, @comment]
Ist @commentable ein Artikel, erzeugt Rails das Formular für die verschachtelte Artikel-Kommentar-Route. Bei einem Foto oder einer Veranstaltung wird automatisch die jeweils passende Route verwendet.
Wir können dadurch dieselbe new-View und denselben CommentsController für alle drei Arten von Inhalten verwenden.
Rufen wir beispielsweise:
/photos/1/comments/new
auf, erstellen wir einen Kommentar für das entsprechende Foto.
Bei:
/articles/1/comments/new
wird dagegen derselbe Code verwendet, um einen Kommentar für einen Artikel zu erstellen.
Unsere polymorphe Beziehung funktioniert damit nicht nur auf Model-Ebene, sondern auch gemeinsam mit den verschachtelten Rails-Routen, dem Controller und unseren Formularen.
3.webp
30.5 KB
Kommentare direkt auf den Show-Seiten anzeigen
Die Kommentarfunktion in Partials aufteilen
Momentan besitzen unsere Kommentare eine eigene Index-Seite und eine eigene Seite zum Erstellen eines neuen Kommentars.
Für unsere Anwendung ist es jedoch komfortabler, die vorhandenen Kommentare und das Formular für einen neuen Kommentar direkt auf der jeweiligen Show-Seite eines Artikels, Fotos oder einer Veranstaltung anzuzeigen.
Damit wir denselben Code für alle drei Models verwenden können, lagern wir die Kommentaranzeige und das Formular in Partials aus.
Bevor wir das tun, ergänzen wir zunächst eine einfache Validierung in unserem Comment-Model:
Die separate Seite zum Erstellen eines Kommentars funktioniert damit weiterhin. Gleichzeitig können wir dasselbe Formular nun auch an anderen Stellen unserer Anwendung einbinden.
Einzelne Kommentare als Partial darstellen
Auch die Darstellung eines Kommentars lagern wir in ein Partial aus:
Damit steht uns dieselbe Inline-Kommentarfunktion für alle drei Models zur Verfügung.
Ein Article, Photo und Event verwendet jeweils dieselben Comment-Datensätze, denselben CommentsController und dieselben Kommentar-Partials. Welchem Objekt ein Kommentar tatsächlich gehört, verwaltet Rails über unsere polymorphe commentable-Assoziation.
5.webp
84.8 KB
Kommentare gestalten
Ein einfaches Styling für die Kommentarfunktion hinzufügen
Unsere polymorphe Kommentarfunktion ist jetzt vollständig eingerichtet. Zum Abschluss können wir die Darstellung der Kommentare und des Formulars noch etwas übersichtlicher gestalten.
In älteren Rails-Anwendungen wurde dafür häufig Sass beziehungsweise SCSS zusammen mit sassc-rails verwendet. Für unsere Kommentarfunktion benötigen wir jedoch keine speziellen Sass-Funktionen. Die wenigen Styles können wir deshalb mit normalem CSS umsetzen.
erhält jeder einzelne Kommentar automatisch die Klasse:
comment
und kann über unser Stylesheet gestaltet werden.
Das Stylesheet einbinden
Wie Stylesheets eingebunden werden, hängt vom Asset-Setup der Rails-Anwendung ab.
Wenn unsere Anwendung die klassische Asset Pipeline verwendet und application.css die Stylesheets des Verzeichnisses automatisch einbindet, reicht es aus, comments.css unter:
app/assets/stylesheets/comments.css
abzulegen.
Verwendet unsere Anwendung stattdessen explizite CSS-Imports, binden wir comments.css entsprechend über das vorhandene Stylesheet-Setup ein.
Für die polymorphe Beziehung selbst spielt das verwendete CSS-System keine Rolle. Unsere Models, Routes, Controller und Views funktionieren unabhängig davon.
Das Ergebnis
Damit besitzen wir jetzt eine Kommentarfunktion, die wir für verschiedene Models wiederverwenden können.
Ein Kommentar gehört über:
belongs_to :commentable, polymorphic: true
zu einem beliebigen kommentierbaren Objekt.
Unsere drei Models stellen die andere Seite der Beziehung bereit:
Wir verwenden Cookies, um Inhalte und Anzeigen zu personalisieren,
Funktionen für soziale Medien anbieten zu können und die Zugriffe auf
unsere Website zu analysieren.
Sie akzeptieren unsere Cookies, wenn Sie fortfahren diese Webseite zu
nutzen.
Datenschutzerklärung.