<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Recordable Pattern &#8211; webmatze.de</title>
	<atom:link href="https://webmatze.de/tag/recordable-pattern/feed/" rel="self" type="application/rss+xml" />
	<link>https://webmatze.de</link>
	<description>Profi Tipps für einen erfolgreichen Internetauftritt</description>
	<lastBuildDate>Fri, 19 Dec 2025 19:15:37 +0000</lastBuildDate>
	<language>de</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.1</generator>
	<item>
		<title>Recordables in Rails: Delegated Types praxisnah erklärt</title>
		<link>https://webmatze.de/recordables-in-rails-delegated-types-praxisnah-erklaert/</link>
					<comments>https://webmatze.de/recordables-in-rails-delegated-types-praxisnah-erklaert/#respond</comments>
		
		<dc:creator><![CDATA[Mathias Karstädt]]></dc:creator>
		<pubDate>Fri, 19 Dec 2025 19:15:37 +0000</pubDate>
				<category><![CDATA[Programmierung]]></category>
		<category><![CDATA[Ruby on Rails]]></category>
		<category><![CDATA[rails]]></category>
		<category><![CDATA[Recordable Pattern]]></category>
		<category><![CDATA[ruby]]></category>
		<guid isPermaLink="false">https://webmatze.de/?p=1122</guid>

					<description><![CDATA[In diesem Beitrag zeige ich dir das Recordable Pattern, das 37signals (Basecamp, HEY) einsetzt, und erkläre es Schritt für Schritt anhand von lauffähigen Rails-Code-Beispielen. Zielgruppe sind Anfänger bis Fortgeschrittene Ruby on Rails Entwickler, die polymorphe Datenmodelle sauber, skalierbar und gut paginierbar bauen möchten. Warum das Ganze? Klassische Ansätze wie Single Table Inheritance (STI) oder „nackte“ [&#8230;]]]></description>
										<content:encoded><![CDATA[<p>In diesem Beitrag zeige ich dir das Recordable Pattern, das 37signals (Basecamp, HEY) einsetzt, und erkläre es Schritt für Schritt anhand von lauffähigen Rails-Code-Beispielen. Zielgruppe sind Anfänger bis Fortgeschrittene Ruby on Rails Entwickler, die polymorphe Datenmodelle sauber, skalierbar und gut paginierbar bauen möchten.</p>
<p><iframe title="The Rails Delegated Type Pattern with Jeffrey Hardy" width="500" height="281" src="https://www.youtube.com/embed/m90sl-Uvu0Y?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></p>
<p>Warum das Ganze? Klassische Ansätze wie Single Table Inheritance (STI) oder „nackte“ polymorphe Assoziationen stoßen schnell an Grenzen: STI bläht Tabellen auf, polymorphes CRUD bleibt oft mühsam zu paginieren. Das Recordable Pattern nutzt Rails’ <a href="http://https://api.rubyonrails.org/classes/ActiveRecord/DelegatedType.html" title="Delegated Types">Delegated Types</a>, um eine dünne, performante „Superklasse“-Tabelle (Recording) mit schlanken „Subklasse“-Tabellen (Recordables wie Message, Document, Comment) zu kombinieren.</p>
<hr />
<h2>Das Grundprinzip</h2>
<ul>
<li>Eine zentrale Tabelle <strong>„recordings“</strong> hält alle gemeinsamen Metadaten (z. B. Bucket, Creator, Timestamps) und verweist polymorph auf den „konkreten Inhalt“ (recordable).</li>
<li>Jede konkrete Recordable (Message, Document, Comment) hat eine eigene Tabelle mit genau ihren Feldern.</li>
<li>Abfragen, Pagination und gemeinsame Logik passieren über Recording; konkretes Verhalten (z. B. <code>export_html</code>) lebt bei den Recordables.</li>
</ul>
<p>So bleibst du:</p>
<ul>
<li>flexibel (neue Typen ohne Recording-Migration),</li>
<li>performant (einheitliche Timeline auf der dünnen Recording-Tabelle),</li>
<li>sauber getrennt (Metadaten vs. Inhalte).</li>
</ul>
<hr />
<h2>Minimales Datenmodell</h2>
<p>Migration (gekürzt auf das Wesentliche):</p>
<pre><code class="language-ruby">create_table :recordings do |t|
  t.references :bucket,  null: false, foreign_key: true
  t.references :creator, null: false, foreign_key: { to_table: :users }
  t.string  :recordable_type, null: false
  t.bigint  :recordable_id,   null: false
  t.index [:recordable_type, :recordable_id]
  t.bigint :parent_id # optional: Recording-Baum (Unterelemente)
  t.index :parent_id
  t.timestamps
end
add_foreign_key :recordings, :recordings, column: :parent_id

create_table :messages do |t|
  t.string :title,   null: false
  t.text   :content, null: false
  t.timestamps
end

create_table :documents do |t|
  t.string :title, null: false
  t.text   :body
  t.string :external_url
  t.timestamps
end

create_table :comments do |t|
  t.text :body, null: false
  t.timestamps
end</code></pre>
<hr />
<h2>Models und Concerns</h2>
<p>Recording: die „Superklasse“ mit Delegation</p>
<pre><code class="language-ruby">class Recording &lt; ApplicationRecord
  belongs_to :bucket
  belongs_to :creator, class_name: &quot;User&quot;

  delegated_type :recordable, types: %w[Message Document Comment], dependent: :destroy

  # optional: Baumstruktur
  belongs_to :parent, class_name: &quot;Recording&quot;, optional: true
  has_many   :children, class_name: &quot;Recording&quot;, foreign_key: :parent_id, dependent: :nullify

  # Bequeme Filter-Scopes kommen mit delegated_type:
  # Recording.messages, Recording.documents, Recording.comments
end</code></pre>
<p>Recordable-Concern: gemeinsame API für Inhalte</p>
<pre><code class="language-ruby">module Recordable
  extend ActiveSupport::Concern

  included do
    has_many :recordings, as: :recordable, inverse_of: :recordable
  end

  def display_title
    respond_to?(:title) ? title : self.class.name
  end

  def export_html
    &quot;#{display_title}&quot;
  end
end</code></pre>
<p>Konkrete Typen:</p>
<pre><code class="language-ruby">class Message &lt; ApplicationRecord
  include Recordable
  validates :title, :content, presence: true

  def export_html
    &quot;#{title}#{content}&quot;
  end
end

class Document &lt; ApplicationRecord
  include Recordable
  validates :title, presence: true

  def export_html
    if external_url.present?
      &quot;#{title}External: #{external_url}&quot;
    else
      &quot;#{title}#{body}&quot;
    end
  end
end

class Comment &lt; ApplicationRecord
  include Recordable
  validates :body, presence: true

  def display_title
    body.truncate(40)
  end

  def export_html
    &quot;#{body}&quot;
  end
end</code></pre>
<hr />
<h2>Erstellen und Paginieren einer Timeline</h2>
<p>Bucket kapselt die Aufnahme neuer Inhalte („record“) und die Timeline. Bucket ist hier eher abstrakt und könnte genausogut auch ein Projekt sein, oder ein Kunde:</p>
<pre><code class="language-ruby">class Bucket &lt; ApplicationRecord
  has_many :recordings, dependent: :destroy

  def timeline(limit: 50)
    recordings.order(created_at: :desc).limit(limit).includes(:recordable)
  end

  def record(recordable, creator:, parent: nil, color: nil)
    recordings.create!(recordable: recordable, creator: creator, parent: parent, color: color)
  end
end</code></pre>
<p>Typische Abfragen:</p>
<pre><code class="language-ruby">bucket = Bucket.find(1)

# Gesamte Timeline effizient paginieren
bucket.timeline(limit: 50).each do |rec|
  puts &quot;[#{rec.recordable_type}] #{rec.recordable.display_title}&quot;
end

# Nur bestimmte Typen (Scopes via delegated_type)
Recording.messages.where(bucket: bucket).limit(20)
Recording.documents.where(bucket: bucket).order(created_at: :desc)</code></pre>
<hr />
<h2>Versionierung per Repointing (immutable Recordables)</h2>
<p>Statt Inhalte zu „updaten“, erzeugst du neue Recordables und verweist die Recording-Zeile auf die neue Version. Das hält Historie sauber und macht Kopieren effizient.</p>
<pre><code class="language-ruby">class Recording &lt; ApplicationRecord
  # ...

  def repoint_to!(new_recordable)
    update!(recordable: new_recordable)
  end
end

# Beispiel: Message ? neue Document-Version
msg_rec = bucket.record(Message.create!(title: &quot;Kickoff&quot;, content: &quot;Welcome!&quot;), creator: user)
new_doc = Document.create!(title: &quot;Specs v2&quot;, body: &quot;Updated requirements&quot;)
msg_rec.repoint_to!(new_doc) # Recording zeigt nun auf Document</code></pre>
<p>Wenn du eine echte Ereignis-Historie brauchst, ergänze ein <code>Event</code>-Model und schreibe beim <code>record</code>/<code>repoint_to!</code> Einträge (z. B. „created“, „repointed“, inkl. vorher/nachher <code>recordable_type/_id</code>). Das hält Audit-Trails und erlaubt Timeline-Features (siehe Video/37signals-Artikel).</p>
<hr />
<h2>Copy &amp; Move: Subtrees kopieren</h2>
<p>Ein Vorteil des Patterns: Kopieren und Verschieben ganzer Teilbäume wird planbar und schnell, weil Recordings leichtgewichtig sind und Recordables immutable dupliziert werden. Eine einfache Kopier-Serviceklasse:</p>
<pre><code class="language-ruby">class Copier
  def self.copy!(source_recording:, destination_bucket:, creator:)
    new(source_recording, destination_bucket, creator).copy!
  end

  def initialize(source_recording, destination_bucket, creator)
    @source_recording   = source_recording
    @destination_bucket = destination_bucket
    @creator            = creator
  end

  def copy!
    ActiveRecord::Base.transaction do
      copied_root = copy_recording(@source_recording, parent: nil)
      @source_recording.children.each { |child| copy_branch(child, parent: copied_root) }
      copied_root
    end
  end

  private

  def copy_branch(node, parent:)
    copied = copy_recording(node, parent: parent)
    node.children.each { |child| copy_branch(child, parent: copied) }
    copied
  end

  def copy_recording(original, parent:)
    new_recordable = duplicate_recordable(original.recordable)
    @destination_bucket.record(new_recordable, creator: @creator, parent: parent, color: original.color)
  end

  def duplicate_recordable(recordable)
    case recordable
    when Message
      Message.create!(title: recordable.title, content: recordable.content)
    when Document
      Document.create!(title: recordable.title, body: recordable.body, external_url: recordable.external_url)
    when Comment
      Comment.create!(body: recordable.body)
    else
      raise ArgumentError, &quot;Unsupported recordable: #{recordable.class.name}&quot;
    end
  end
end</code></pre>
<hr />
<h2>Vergleich mit STI und „plain“ Polymorphismus</h2>
<ul>
<li><strong>STI</strong>: Einfache Fälle ok. Bei divergenten Typen entsteht Tabellen-Bloat, viele NULL-Spalten, Migrationen werden groß, Erweiterbarkeit leidet.</li>
<li><strong>Polymorphismus ohne <code>delegated_type</code></strong>: Funktioniert, aber es fehlen bequeme Scopes/Convenience-Methoden; unified Pagination ist oft hakelig.</li>
<li><strong>Recordables + <code>delegated_type</code></strong>: Einheitliche Timeline über eine dünne Tabelle, klare Trennung von Meta vs. Inhalt, leichte Erweiterbarkeit (neuen Typ hinzufügen statt zentrale Tabelle migrieren), gute Performance bei Abfragen.</li>
</ul>
<hr />
<h2>Wann solltest du das Pattern nutzen?</h2>
<ul>
<li>Du brauchst eine gemischte, paginierbare Timeline (z. B. Aktivitätsfeed über Messages, Documents, Comments).</li>
<li>Deine Typen unterscheiden sich stark in ihren Attributen.</li>
<li>Du willst Polymorphismus am Parent (Recording) und saubere Delegation an die Inhalte.</li>
</ul>
<p>Wann eher nicht?</p>
<ul>
<li>Deine Subtypen sind fast identisch (STI könnte reichen).</li>
<li>Du hast keine gemeinsamen Abfragen über Typgrenzen hinweg.</li>
</ul>
<hr />
<h2>Best Practices</h2>
<ul>
<li>Halte Recording schlank: nur Metadaten und die polymorphe Referenz. Keine großen Textfelder.</li>
<li>Packe typenübergreifende Logik in Recording (oder Concerns), typenspezifische Logik in die Recordables.</li>
<li>Nutze <code>delegate</code> am Recording für gemeinsame Schnittstellen, z. B. <code>delegate :export_html, to: :recordable</code>.</li>
<li>Wenn Auditing/History wichtig ist, ergänze Events und schreibe Änderungen mit.</li>
<li>Autorisierung: Recording bündelt viele Aktionen – prüfe Berechtigungen konsequent.</li>
</ul>
<hr />
<h2>Fazit</h2>
<p>Das Recordable Pattern mit <code>delegated_type</code> ist eine elegante, praxiserprobte Lösung für polymorphe Inhalte in Rails. Es bringt dir:</p>
<ul>
<li>eine einheitliche, performante Timeline,</li>
<li>klare Verantwortlichkeiten zwischen Metadaten und Inhaltsobjekten,</li>
<li>einfache Erweiterbarkeit ohne große Migrationen,</li>
<li>saubere APIs durch Delegation.</li>
</ul>
<p>Wenn du Feed-ähnliche Strukturen, Versionierung und Kopier-Features brauchst, wirst du mit Recordings/Recordables sehr schnell produktiv.</p>
<p>Viel Spaß beim Ausprobieren – und schreib mir gern, wenn du Fragen hast oder Beispiele aus deinem Projekt teilen willst!</p>
<p>— Mathias (webmatze.de)</p>
]]></content:encoded>
					
					<wfw:commentRss>https://webmatze.de/recordables-in-rails-delegated-types-praxisnah-erklaert/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
