Was ist Ruby on Rails?
Rails ist ein Full-Stack-Web-Framework für Ruby. Es liefert alles mit, was eine datenbankgestützte Webanwendung benötigt: ein ORM, ein Router, ein View-Layer, Background-Jobs, Mailer, Caching, WebSockets und ein Test-Harness. Anstatt diese Komponenten selbst auszuwählen und zu verdrahten, erhalten Sie einen kohärenten Stack, der darauf ausgelegt ist, nahtlos zusammenzuarbeiten.
Die definierende Idee ist Convention over Configuration. Das Framework geht von einer sinnvollen Struktur aus — Post befindet sich in app/models/post.rb, wird einer posts-Tabelle zugeordnet und wird von PostsController bereitgestellt — und Sie schreiben nur dann Konfigurationen, wenn Sie von diesen Konventionen abweichen. Das Rails-Team nennt diese Philosophie Omakase, die Wahl des Kochs: Vertrauen Sie den Standardeinstellungen, und Sie werden schneller vorankommen.
Dieser Ansatz ist der Grund, warum Rails seit zwanzig Jahren Produkte von Basecamp und GitHub bis hin zu Shopify und HEY antreibt und warum ein einzelner Entwickler immer noch in einem Wochenende eine ernsthafte Anwendung veröffentlichen kann.
Convention over Configuration
Jedes Rails-Projekt hat das gleiche Grundgerüst, sodass du dich nie fragen musst, wo eine bestimmte Datei hingehört.
app/
├── controllers/ # request handling
├── models/ # domain and persistence
├── views/ # templates
├── jobs/ # background work
├── mailers/ # email
└── javascript/ # Stimulus controllers
config/
├── routes.rb # the URL map
└── database.yml # connection settings
db/
└── migrate/ # schema history
Der Vorteil ist die Vorhersehbarkeit. Ein neuer Entwickler öffnet das Repository und weiß sofort, dass sich das User-Model in app/models/user.rb befindet und die dazugehörige Tabelle users heißt. Benennungen sind hier keine Stilfrage, sondern eine Schnittstelle, auf die sich das Framework verlässt.
Der MVC-Request-Zyklus
Rails ist ein Model-View-Controller-Framework, und jeder HTTP-Request folgt demselben Pfad:
- Das Routing ordnet die URL einer Controller-Action zu.
- Der Controller lädt oder ändert Daten über Models.
- Models setzen die Domänenlogik durch und kommunizieren mit der Datenbank.
- Ein View rendert die Antwort, meist als HTML.
# config/routes.rb
Rails.application.routes.draw do
resources :posts
root "posts#index"
end
Ein Request an GET /posts landet bei PostsController#index, welcher @posts vorbereitet, woraufhin app/views/posts/index.html.erb gerendert wird. Jede Schicht auf ihre Kernaufgabe zu fokussieren, sorgt dafür, dass eine Rails-App auch bei wachsender Größe wartbar bleibt.
Die Kommandozeile ist das Framework
Sie werden Rails kaum über Konfigurationsdateien berühren. Der Großteil der Arbeit findet im Terminal statt.
rails new shop --database=postgresql
cd shop
bin/rails generate scaffold Post title:string body:text published:boolean
bin/rails generate model Comment post:references body:text
bin/rails generate controller Pages home about
bin/rails db:create db:migrate
bin/rails server
generate schreibt das Model, die Migration, den Controller, die Routes, die Views und die Tests für Sie, und die Ausgabe des Generators teilt Ihnen mit, was geändert wurde. Lesen Sie jede generierte Datei, bevor Sie diese übernehmen – der schnellste Weg, Rails zu lernen, besteht darin, zu studieren, was das Framework schreibt, und anschließend alles zu löschen, was Sie nicht benötigen.
Active Record: Die Model-Layer
Active Record ist das ORM. Eine Model-Klasse kapselt eine Datenbanktabelle, und eine Instanz kapselt eine einzelne Zeile.
Migrations
Migrations sind versionierte, umkehrbare Schema-Änderungen, die in db/migrate gespeichert werden. Sie sind der einzige unterstützte Weg, um die Datenbank zu ändern.
class CreatePosts < ActiveRecord::Migration[8.0]
def change
create_table :posts do |t|
t.string :title, null: false
t.text :body, null: false
t.boolean :published, default: false, null: false
t.references :author, null: false, foreign_key: { to_table: :users }
t.timestamps
end
add_index :posts, %i[published created_at]
end
end
Führe bin/rails db:migrate aus, um sie anzuwenden, und bin/rails db:rollback, um sie rückgängig zu machen. Da change umkehrbar ist, weiß Rails, wie die Operationen rückgängig gemacht werden können, ohne dass du eine down-Methode schreiben musst.
Associations
Associations beschreiben Beziehungen und generieren komfortable Methoden.
class User < ApplicationRecord
has_many :posts, dependent: :destroy
has_many :comments, through: :posts
end
class Post < ApplicationRecord
belongs_to :author, class_name: "User", inverse_of: :posts
has_many :comments, dependent: :destroy
has_one_attached :cover
end
Sobald dies eingerichtet ist, funktionieren user.posts, post.author und post.comments.create(body: "...") alle, und dependent: :destroy verhindert, dass verwaiste Zeilen in der Datenbank verbleiben.
Validations und Scopes
Validations schützen die Domain auf der Model-Layer, über die jeder Schreibzugriff läuft.
class Post < ApplicationRecord
validates :title, presence: true, length: { maximum: 120 }
validates :body, presence: true
validates :slug, uniqueness: true
scope :published, -> { where(published: true) }
scope :recent, -> { order(created_at: :desc) }
before_validation :generate_slug, on: :create
private
def generate_slug
self.slug ||= title.to_s.parameterize
end
end
Scopes geben Relations zurück und sind daher kombinierbar: Post.published.recent.limit(20) liest sich wie ein Satz und wird als eine einzige Query ausgeführt. Wenn ein Scope länger als ein oder zwei Zeilen wird, solltest du ihn in eine Klassenmethode umwandeln, damit er lesbar bleibt.
N+1 Queries vermeiden
Das häufigste Performance-Problem in Rails ist die N+1 Query: eine Query für eine Liste und dann eine weitere Query für die Association jeder einzelnen Zeile.
# Two queries total, no matter how many posts.
posts = Post.includes(:author, :comments).recent
posts.each do |post|
puts "#{post.title} by #{post.author.name}"
end
Nutze includes, wenn du auf die Association zugreifen wirst, preload, wenn du eine separate Query möchtest, und eager_load, wenn du zusätzlich nach der Association filterst. Füge das bullet-Gem zur Development-Umgebung hinzu, damit jede N+1 Query markiert wird, bevor sie die Production erreicht.
Controller und RESTful Routes
resources definiert die konventionellen CRUD-Routes und ordnet sie Controller-Actions mit entsprechenden Namen zu.
# config/routes.rb
Rails.application.routes.draw do
resources :posts do
resources :comments, only: %i[index create destroy]
end
namespace :api do
namespace :v1 do
resources :posts, only: %i[index show create update destroy]
end
end
end
Controller bleiben “thin”. Sie laden Daten, rufen die Domain auf und wählen eine Antwort aus. Die Business-Logik gehört in Models oder Service-Objekte, nicht in die Actions.
class PostsController < ApplicationController
before_action :set_post, only: %i[show update destroy]
def index
@posts = Post.published.includes(:author).recent
end
def create
@post = Post.new(post_params)
if @post.save
redirect_to @post, notice: "Post created."
else
render :new, status: :unprocessable_entity
end
end
private
def set_post
@post = Post.find(params[:id])
end
def post_params
params.require(:post).permit(:title, :body, :published, tag_ids: [])
end
end
Strong Parameters sind nicht optional. params.require(:post).permit(...) ist die Grenze, die verhindert, dass ein Client Spalten setzt, die niemals dafür vorgesehen waren, wie zum Beispiel admin oder role.
Views, ERB und Layouts
Views sind ERB-Templates: HTML mit eingebettetem Ruby. Rails escaped die Ausgabe standardmäßig, wodurch eine ganze Klasse von Cross-Site-Scripting-Bugs eliminiert wird.
<%# app/views/posts/index.html.erb %>
<h1>Posts</h1>
<%= link_to "New post", new_post_path %>
<ul>
<% @posts.each do |post| %>
<li>
<%= link_to post.title, post %>
<span><%= pluralize(post.comments.size, "comment") %></span>
</li>
<% end %>
</ul>
Nutzen Sie <%= %> für die Ausgabe und <% %> zur Ausführung ohne Ausgabe. Ein Layout in app/views/layouts/application.html.erb umschließt jede Seite, und Partials – Dateien, die mit einem Unterstrich beginnen – ermöglichen es Ihnen, Markup mit render "post", post: post oder einfach render @posts wiederzuverwenden.
Hotwire: Turbo und Stimulus
Modernes Rails ermöglicht Interaktivität, ohne dass eine Single-Page-App erforderlich ist. Turbo fängt Links und Formulare ab und tauscht Seitenfragmente über das Netzwerk aus; Stimulus fügt kleine JavaScript-Verhaltensweisen dort hinzu, wo sie tatsächlich benötigt werden.
<%# Turbo Frames scope an update to one part of the page %>
<%= turbo_frame_tag "comments" do %>
<%= render @post.comments %>
<% end %>
// app/javascript/controllers/dropdown_controller.js
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
static targets = ["menu"];
toggle() {
this.menuTarget.classList.toggle("hidden");
}
}
Das ist Progressive Enhancement in der Praxis: Server-gerendertes HTML bildet die Basis, und JavaScript wertet diese auf. Die meisten Rails-Apps benötigen niemals einen clientseitigen Router.
Assets und import maps
Rails 7 hat die alte Sprockets-Pipeline standardmäßig durch import maps ersetzt und bietet Propshaft als einfachere Asset-Pipeline an. So können Sie eine Library einbinden (pinnen), ohne einen Node-Build-Schritt zu benötigen.
bin/importmap pin local-time
bin/rails assets:precompile
Wenn Sie lieber einen Bundler verwenden möchten, schalten bin/rails javascript:install:esbuild oder :vite das Setup um. Das Prinzip bleibt dasselbe: Liefern Sie das aus, was der Browser versteht, und halten Sie die Toolchain so weit wie möglich heraus, bis sie tatsächlich benötigt wird.
Background-Jobs
Alles, was langsam ist – das Versenden von E-Mails, API-Aufrufe, das Erstellen von Berichten –, gehört in einen Job und nicht in den Request. Active Job dient dabei als gemeinsame Schnittstelle; der Adapter entscheidet, wo die Jobs ausgeführt werden.
class PublishPostJob < ApplicationJob
queue_as :default
retry_on Net::ReadTimeout, wait: :polynomially_longer, attempts: 5
def perform(post_id)
Post.find(post_id).publish!
end
end
PublishPostJob.perform_later(post.id)
# config/application.rb
config.active_job.queue_adapter = :solid_queue # or :sidekiq
Rails 8 wird mit Solid Queue ausgeliefert, wodurch Jobs direkt in der Datenbank gespeichert werden und kein Redis benötigt wird. So hat eine Single-Server-App eine Komponente weniger, die gewartet werden muss. Sidekiq bleibt bei hohem Volumen eine starke Option.
Testing
Rails generiert für alles, was es erstellt, automatisch Tests. Minitest ist bereits integriert und schnell; RSpec ist die beliebte Alternative mit einer ausdrucksstärkeren Syntax.
# test/models/post_test.rb
require "test_helper"
class PostTest < ActiveSupport::TestCase
test "requires a title" do
post = Post.new(body: "Hello")
assert_not post.valid?
assert_includes post.errors[:title], "can't be blank"
end
end
# spec/models/post_spec.rb
RSpec.describe Post, type: :model do
it "is invalid without a title" do
expect(Post.new(body: "Hello")).not_to be_valid
end
end
Controller-Tests führen echte Anfragen im Prozess aus, wodurch Routing- und Parameter-Fehler abgefangen werden, die Unit-Tests übersehen.
# test/controllers/posts_controller_test.rb
class PostsControllerTest < ActionDispatch::IntegrationTest
test "lists published posts" do
Post.create!(title: "Hi", body: "There", published: true)
get posts_url
assert_response :success
end
end
Führen Sie alles mit bin/rails test oder bundle exec rspec aus. Fügen Sie bin/rails test:system für browsergesteuerte System-Tests hinzu, wenn ein Flow wichtig genug ist, um ihn End-to-End zu testen.
Die Rails Console
bin/rails console bootet deine Anwendung, wobei jedes Model und jeder Helper verfügbar ist. Es ist der schnellste Weg, um Daten zu explorieren, eine Query zu testen und einen Bug zu reproduzieren.
bin/rails console
bin/rails console --sandbox # rolls back all changes on exit
Post.published.recent.limit(5)
Post.where(published: false).update_all(published: true)
Post.includes(:author).find_each { |post| puts post.title }
--sandbox dient als Sicherheitsnetz während deiner Experimente: Jeder Schreibvorgang wird beim Beenden zurückgerollt (rolled back).
Best Practices
- Halten Sie Controller schlank; verschieben Sie die Domain-Logik in Models oder Service-Objekte.
- Nutzen Sie Eager Loading für Assoziationen mit
includesund achten Sie in den Logs auf N+1-Queries. - Ändern Sie die Datenbank ausschließlich über Migrationen und committen Sie diese zusammen mit dem Code.
- Verwenden Sie Strong Parameters für jeden Schreibvorgang und validieren Sie auf der Model-Ebene.
- Fügen Sie Datenbank-Indizes für die Spalten hinzu, nach denen Sie filtern und sortieren.
- Lagern Sie zeitintensive Aufgaben in Background Jobs aus, anstatt sie im Request-Zyklus auszuführen.
- Schreiben Sie für jeden behobenen Bug einen Test und halten Sie die Test-Suite performant.
Häufige Fehler
- Das Rendern einer Liste ohne
includes, was zu einem N+1-Problem in der Production führt. - Das Platzieren von Business-Logik in Controllern, bis die Actions hunderte Zeilen lang sind.
- Die direkte Verwendung von
params[:post]anstelle von Strong Parameters. - Das manuelle Editieren von
db/schema.rbanstatt einer Migration zu schreiben. - Das Vergessen von
dependent:-Optionen, wodurch verwaiste Datensätze zurückbleiben. - Der Griff zu einem Service Object oder einem Gem, bevor das Model dies wirklich benötigt.
- Das Ausführen komplexer Queries in Views anstatt die Daten im Controller zu laden.
Wie geht es weiter?
Rails vermittelt eine beständige Denkweise: Ressourcen, Konventionen und ein fester Platz für jedes Anliegen. Wenn Ihnen das „Batteries-included“-Modell gefällt, verfolgt Laravel dieselbe Philosophie in PHP, während Django dies in Python tut. Da das Routing in Rails auf Ressourcen basiert, erläutert der Guide zu REST APIs die dahinterstehenden Konzepte. Und da die meisten Rails-Apps auf einer relationalen Datenbank laufen, sollten Sie als Nächstes Ihr Wissen über PostgreSQL vertiefen.