# Ruby on Rails Interview-Fragen: Top 25 in 2026 > Die 25 häufigsten Ruby on Rails Interview-Fragen. MVC-Architektur, Active Record, Migrationen, RSpec-Tests, REST-APIs mit detaillierten Antworten und Codebeispielen. - Published: 2026-02-03 - Updated: 2026-04-27 - Author: Anthony Fillion-Maillet - Tags: ruby on rails, ruby, interview, active record, technisches interview - Reading time: 22 min --- Ruby-on-Rails-Interviews bewerten die Beherrschung des beliebtesten Ruby-Frameworks, das Verständnis der MVC-Architektur, des Active-Record-ORMs sowie die Fähigkeit, robuste Webanwendungen nach der Philosophie "Convention over Configuration" zu entwickeln. Dieser Leitfaden behandelt die 25 häufigsten Fragen, von Rails-Grundlagen bis zu fortgeschrittenen Produktionsmustern. > **Tipp für das Interview** > > Recruiter schätzen Kandidaten, die die Rails-Philosophie verstehen: "Convention over Configuration", DRY (Don't Repeat Yourself) und die Rails-Way-Patterns. Zu erklären, warum Rails bestimmte Architekturentscheidungen trifft, macht den Unterschied. ## Ruby on Rails: Grundlagen ### Frage 1: Erklären Sie das MVC-Pattern in Ruby on Rails Das Model-View-Controller-Pattern (MVC) ist der architektonische Kern von Rails. Es trennt die Verantwortlichkeiten in drei Schichten für bessere Wartbarkeit und Testbarkeit des Codes. ```ruby # app/models/article.rb # Das Model verwaltet Daten und Geschäftslogik class Article < ApplicationRecord # Datenvalidierungen validates :title, presence: true, length: { minimum: 5 } validates :body, presence: true # Assoziationen mit anderen Models belongs_to :author, class_name: 'User' has_many :comments, dependent: :destroy has_many :tags, through: :article_tags # Scopes für wiederverwendbare Abfragen scope :published, -> { where(published: true) } scope :recent, -> { order(created_at: :desc).limit(10) } # Lebenszyklus-Callbacks before_save :generate_slug private def generate_slug self.slug = title.parameterize if title_changed? end end ``` ```ruby # app/controllers/articles_controller.rb # Der Controller empfängt Requests und orchestriert die Antwort class ArticlesController < ApplicationController before_action :authenticate_user!, except: [:index, :show] before_action :set_article, only: [:show, :edit, :update, :destroy] def index @articles = Article.published.recent.includes(:author) end def show @comments = @article.comments.includes(:user) end def create @article = current_user.articles.build(article_params) if @article.save redirect_to @article, notice: 'Artikel erfolgreich erstellt.' else render :new, status: :unprocessable_entity end end private def set_article @article = Article.find(params[:id]) end def article_params params.require(:article).permit(:title, :body, :published, tag_ids: []) end end ``` ```erb <%# app/views/articles/show.html.erb %> <%# Die View zeigt die Daten im HTML-Format an %>

<%= @article.title %>

Von <%= @article.author.name %> • <%= l @article.created_at, format: :long %>

<%= simple_format @article.body %>
<%# Partial für Kommentare %> <%= render @comments %>
``` Der typische Ablauf: Der Request erreicht den Router, der ihn an den passenden Controller weiterleitet. Der Controller interagiert mit dem Model, um Daten abzurufen oder zu ändern, und übergibt diese dann der View für das HTML-Rendering. ### Frage 2: Was ist Active Record und wie funktioniert das ORM von Rails? Active Record ist das ORM (Object-Relational Mapping) von Rails, das das Active-Record-Pattern implementiert. Jede Model-Klasse repräsentiert eine Datenbanktabelle, und jede Instanz repräsentiert eine Zeile. ```ruby # app/models/user.rb # Active Record ordnet Spalten automatisch Attributen zu class User < ApplicationRecord # Die Tabelle 'users' wird automatisch zugeordnet # Spalten: id, email, name, created_at, updated_at has_secure_password # BCrypt für das Passwort has_many :articles, foreign_key: :author_id has_one :profile, dependent: :destroy has_and_belongs_to_many :roles # Validierungen validates :email, presence: true, uniqueness: { case_sensitive: false }, format: { with: URI::MailTo::EMAIL_REGEXP } # Callbacks before_save :normalize_email # Klassenmethoden für Abfragen def self.admins joins(:roles).where(roles: { name: 'admin' }) end private def normalize_email self.email = email.downcase.strip end end ``` ```ruby # Beispiele für Active-Record-Abfragen # Rails-Konsole oder innerhalb eines Service # Erstellung user = User.create!(email: 'dev@example.com', name: 'Alice', password: 'secret123') # Lesen mit Bedingungen active_users = User.where(active: true).order(:name) user = User.find_by(email: 'dev@example.com') # Verkettete Abfragen (Lazy Evaluation) recent_admins = User.admins .where('created_at > ?', 1.month.ago) .includes(:profile) .limit(10) # N+1-Vermeidung mit Eager Loading articles = Article.includes(:author, :comments).published # Aktualisierung user.update!(name: 'Alice Martin') # Transaktionen User.transaction do user.debit_balance!(100) recipient.credit_balance!(100) Payment.create!(from: user, to: recipient, amount: 100) end ``` Active Record wandelt Ruby-Methoden in optimierte SQL-Abfragen um. Methoden wie `where`, `joins`, `includes` sind lazy: Die Abfrage wird erst beim Iterieren oder beim Aufruf von `to_a` ausgeführt. ### Frage 3: Erklären Sie das Migrationssystem von Rails Migrationen ermöglichen es, das Datenbankschema mit Ruby zu versionieren. Sie sind reversibel und ermöglichen eine kontrollierte Weiterentwicklung der Datenstruktur. ```ruby # db/migrate/20260203100000_create_products.rb # Migration zum Anlegen einer Tabelle class CreateProducts < ActiveRecord::Migration[7.1] def change create_table :products do |t| t.string :name, null: false t.text :description t.decimal :price, precision: 10, scale: 2, null: false t.integer :stock_quantity, default: 0 t.references :category, null: false, foreign_key: true t.boolean :active, default: true t.timestamps # created_at und updated_at automatisch end # Indizes für die Performance add_index :products, :name add_index :products, [:category_id, :active] end end ``` ```ruby # db/migrate/20260203110000_add_slug_to_products.rb # Migration zum Ändern einer existierenden Tabelle class AddSlugToProducts < ActiveRecord::Migration[7.1] def change add_column :products, :slug, :string add_index :products, :slug, unique: true # Bestehende Slugs befüllen reversible do |dir| dir.up do Product.find_each do |product| product.update_column(:slug, product.name.parameterize) end end end # Nach Befüllung NOT NULL setzen change_column_null :products, :slug, false end end ``` ```bash # Wesentliche Migrationsbefehle rails db:migrate # Ausstehende Migrationen ausführen rails db:rollback # Letzte Migration rückgängig machen rails db:rollback STEP=3 # Letzte 3 Migrationen rückgängig machen rails db:migrate:status # Status der Migrationen anzeigen rails db:seed # db/seeds.rb ausführen rails db:reset # Drop, create, migrate, seed ``` Migrationen müssen reversibel sein. Die Methode `change` ist intelligent und kann gängige Operationen automatisch umkehren. Für komplexe Fälle `up` und `down` separat verwenden. ## Fortgeschrittenes Active Record ### Frage 4: Wie optimiert man N+1-Abfragen in Rails? Das N+1-Problem tritt auf, wenn auf eine initiale Abfrage N zusätzliche Abfragen folgen, um Assoziationen zu laden. Rails bietet mehrere Eager-Loading-Methoden zur Lösung dieses Problems. ```ruby # app/controllers/orders_controller.rb class OrdersController < ApplicationController def index # ❌ N+1-PROBLEM: 1 Abfrage + N Abfragen pro Bestellung # @orders = Order.all # In der View: order.user.name erzeugt eine Abfrage pro Bestellung # ✅ LÖSUNG mit includes (Eager Loading) @orders = Order.includes(:user, :items) .where(status: 'completed') .order(created_at: :desc) # Erzeugt insgesamt nur 3 Abfragen end def show # includes: lädt Assoziationen separat (2-3 Abfragen) @order = Order.includes(items: :product).find(params[:id]) # preload: erzwingt separates Laden @order = Order.preload(:items, :user).find(params[:id]) # eager_load: erzwingt LEFT OUTER JOIN (1 Abfrage) @order = Order.eager_load(:items).find(params[:id]) end end ``` ```ruby # app/models/order.rb class Order < ApplicationRecord belongs_to :user has_many :items, class_name: 'OrderItem' has_many :products, through: :items # Scope mit standardmäßigem includes scope :with_details, -> { includes(:user, items: :product) } # Counter Cache zur Vermeidung von COUNT-Abfragen # Erfordert: add_column :users, :orders_count, :integer, default: 0 belongs_to :user, counter_cache: true end ``` ```ruby # N+1-Erkennung mit der Bullet-Gem (Entwicklung) # config/environments/development.rb config.after_initialize do Bullet.enable = true Bullet.alert = true Bullet.bullet_logger = true Bullet.rails_logger = true end # Bullet zeigt Warnungen, wenn: # - Eine N+1-Abfrage erkannt wird # - Unnötiges Eager Loading vorliegt # - Ein Counter Cache verwendet werden sollte ``` Die Regel: standardmäßig `includes` verwenden (Rails wählt die optimale Strategie), `preload` wenn separate Abfragen erzwungen werden sollen, `eager_load` wenn auf Assoziationen gefiltert wird. ### Frage 5: Erklären Sie Scopes und Query Objects in Rails Scopes kapseln wiederverwendbare Abfragebedingungen. Für komplexe Abfragen bieten Query Objects bessere Organisation und Testbarkeit. ```ruby # app/models/product.rb class Product < ApplicationRecord # Einfache Scopes scope :active, -> { where(active: true) } scope :in_stock, -> { where('stock_quantity > 0') } scope :featured, -> { where(featured: true) } # Scopes mit Parametern scope :cheaper_than, ->(price) { where('price < ?', price) } scope :in_category, ->(category) { where(category: category) } # Verkettbare Scopes scope :available, -> { active.in_stock } # Scope mit Joins scope :with_recent_orders, -> { joins(:order_items) .where('order_items.created_at > ?', 30.days.ago) .distinct } # Scope mit Subquery scope :bestsellers, -> { where(id: OrderItem.group(:product_id) .order('COUNT(*) DESC') .limit(10) .select(:product_id)) } end ``` ```ruby # app/queries/products_search_query.rb # Query Object für komplexe Suchen class ProductsSearchQuery def initialize(relation = Product.all) @relation = relation end def call(params) @relation = filter_by_category(params[:category]) @relation = filter_by_price_range(params[:min_price], params[:max_price]) @relation = filter_by_search(params[:q]) @relation = apply_sorting(params[:sort]) @relation end private def filter_by_category(category) return @relation if category.blank? @relation.where(category_id: category) end def filter_by_price_range(min, max) @relation = @relation.where('price >= ?', min) if min.present? @relation = @relation.where('price <= ?', max) if max.present? @relation end def filter_by_search(query) return @relation if query.blank? @relation.where('name ILIKE ? OR description ILIKE ?', "%#{query}%", "%#{query}%") end def apply_sorting(sort) case sort when 'price_asc' then @relation.order(price: :asc) when 'price_desc' then @relation.order(price: :desc) when 'newest' then @relation.order(created_at: :desc) else @relation.order(:name) end end end # Verwendung im Controller @products = ProductsSearchQuery.new(Product.active).call(params) ``` Scopes eignen sich perfekt für einfache, wiederverwendbare Bedingungen. Query Objects passen zu komplexen Suchen mit mehreren optionalen Filtern und Kompositionslogik. ## Routing und Controllers ### Frage 6: Wie funktioniert RESTful Routing in Rails? Rails fördert RESTful-Routen, die HTTP-Verben auf CRUD-Aktionen abbilden. Der Router übersetzt URLs in spezifische Controller-Aufrufe. ```ruby # config/routes.rb Rails.application.routes.draw do # Standardmäßige RESTful-Routen (7 Aktionen) resources :articles do # Verschachtelte Routen resources :comments, only: [:create, :destroy] # Member-Routen (wirken auf eine Instanz) member do post :publish delete :archive end # Collection-Routen (wirken auf die Sammlung) collection do get :drafts get :search end end # API-Routen mit Namespace namespace :api do namespace :v1 do resources :products, only: [:index, :show, :create, :update] do resources :reviews, shallow: true end end end # Benutzerdefinierte Route get 'dashboard', to: 'dashboard#index' # Routenbeschränkungen constraints(SubdomainConstraint.new) do resources :admin_settings end # Root-Route root 'home#index' end ``` ```bash # rails routes - Zeigt alle generierten Routen # # Verb URI Pattern Controller#Action # GET /articles articles#index # POST /articles articles#create # GET /articles/new articles#new # GET /articles/:id/edit articles#edit # GET /articles/:id articles#show # PATCH /articles/:id articles#update # DELETE /articles/:id articles#destroy # POST /articles/:id/publish articles#publish # GET /articles/drafts articles#drafts ``` Generierte Route-Helper (`article_path(@article)`, `new_article_path`) erlauben es, URLs dynamisch und wartbar zu referenzieren. ### Frage 7: Erklären Sie Callbacks und Filter in Controllern Callbacks (before_action, after_action, around_action) erlauben es, Code vor, nach oder rund um Controller-Aktionen auszuführen. ```ruby # app/controllers/application_controller.rb class ApplicationController < ActionController::Base # CSRF-Schutz standardmäßig aktiviert protect_from_forgery with: :exception # Globaler Callback für die Authentifizierung before_action :authenticate_user! # Globale Fehlerbehandlung rescue_from ActiveRecord::RecordNotFound, with: :not_found rescue_from ActionController::ParameterMissing, with: :bad_request private def not_found render json: { error: 'Ressource nicht gefunden' }, status: :not_found end def bad_request(exception) render json: { error: exception.message }, status: :bad_request end end ``` ```ruby # app/controllers/admin/products_controller.rb class Admin::ProductsController < ApplicationController # Callbacks mit Optionen before_action :require_admin before_action :set_product, only: [:show, :edit, :update, :destroy] after_action :log_activity, only: [:create, :update, :destroy] # Bedingter Callback before_action :check_stock, only: [:update], if: :stock_changed? def create @product = Product.new(product_params) if @product.save redirect_to [:admin, @product], notice: 'Produkt erstellt.' else render :new, status: :unprocessable_entity end end def update if @product.update(product_params) redirect_to [:admin, @product], notice: 'Produkt aktualisiert.' else render :edit, status: :unprocessable_entity end end private def require_admin redirect_to root_path unless current_user&.admin? end def set_product @product = Product.find(params[:id]) end def stock_changed? params[:product][:stock_quantity].present? end def log_activity ActivityLog.create!( user: current_user, action: action_name, resource: @product ) end def product_params params.require(:product).permit(:name, :price, :description, :stock_quantity) end end ``` Callbacks werden in der Reihenfolge ihrer Deklaration ausgeführt. `skip_before_action` in Subklassen verwenden, um geerbte Callbacks zu deaktivieren. Callbacks mit zu viel Geschäftslogik vermeiden: Service Objects bevorzugen. ## Services und Architektur ### Frage 8: Wie implementiert man Service Objects in Rails? Service Objects kapseln komplexe Geschäftslogik, die weder zu Models noch zu Controllern gehört. Sie verbessern die Testbarkeit und folgen dem Single-Responsibility-Prinzip. ```ruby # app/services/order_processor.rb # Service Object mit standardisierter Schnittstelle class OrderProcessor def initialize(order, payment_method:) @order = order @payment_method = payment_method end def call return failure('Bestellung bereits verarbeitet') if @order.processed? ActiveRecord::Base.transaction do validate_stock! process_payment! update_inventory! send_confirmation! @order.update!(status: 'completed', processed_at: Time.current) end success(@order) rescue PaymentError => e failure("Zahlung fehlgeschlagen: #{e.message}") rescue InsufficientStockError => e failure("Lagerbestand unzureichend: #{e.message}") end private def validate_stock! @order.items.each do |item| unless item.product.stock_quantity >= item.quantity raise InsufficientStockError, item.product.name end end end def process_payment! result = PaymentGateway.charge( amount: @order.total, method: @payment_method, description: "Bestellung ##{@order.id}" ) raise PaymentError, result.error unless result.success? @order.update!(payment_reference: result.transaction_id) end def update_inventory! @order.items.each do |item| item.product.decrement!(:stock_quantity, item.quantity) end end def send_confirmation! OrderMailer.confirmation(@order).deliver_later end def success(data) Result.new(success: true, data: data) end def failure(error) Result.new(success: false, error: error) end Result = Struct.new(:success, :data, :error, keyword_init: true) do def success? = success def failure? = !success end end ``` ```ruby # app/controllers/orders_controller.rb class OrdersController < ApplicationController def create @order = current_user.orders.build(order_params) if @order.save result = OrderProcessor.new(@order, payment_method: params[:payment_method]).call if result.success? redirect_to @order, notice: 'Bestellung bestätigt!' else @order.update!(status: 'payment_failed') flash.now[:alert] = result.error render :new, status: :unprocessable_entity end else render :new, status: :unprocessable_entity end end end ``` Das Service-Object-Pattern folgt einer einfachen Konvention: eine Klasse, eine Verantwortung, eine öffentliche `call`-Methode. Ein Result-Objekt zurückzugeben ermöglicht eine saubere Behandlung von Erfolg und Fehler. ### Frage 9: Erklären Sie Concerns in Rails Concerns ermöglichen es, Code zwischen Models oder Controllern zu extrahieren und zu teilen. Sie nutzen `ActiveSupport::Concern` für eine saubere Inklusionssyntax. ```ruby # app/models/concerns/sluggable.rb # Wiederverwendbares Concern zur Slug-Generierung module Sluggable extend ActiveSupport::Concern included do # Beim Einbinden ausgeführter Code before_validation :generate_slug, if: :should_generate_slug? validates :slug, presence: true, uniqueness: true end # Klassenmethoden class_methods do def find_by_slug!(slug) find_by!(slug: slug) end def sluggable_source(column = :title) @sluggable_source = column end def sluggable_source_column @sluggable_source || :title end end # Instanzmethoden def to_param slug end private def should_generate_slug? slug.blank? || send("#{self.class.sluggable_source_column}_changed?") end def generate_slug source = send(self.class.sluggable_source_column) return if source.blank? base_slug = source.parameterize self.slug = unique_slug(base_slug) end def unique_slug(base) slug = base counter = 1 while self.class.where(slug: slug).where.not(id: id).exists? slug = "#{base}-#{counter}" counter += 1 end slug end end ``` ```ruby # app/models/article.rb class Article < ApplicationRecord include Sluggable sluggable_source :title # Optional, :title als Standard end # app/models/product.rb class Product < ApplicationRecord include Sluggable sluggable_source :name end ``` ```ruby # app/controllers/concerns/pagination.rb # Concern für Controller module Pagination extend ActiveSupport::Concern included do helper_method :page_param, :per_page_param end private def paginate(relation) relation.page(page_param).per(per_page_param) end def page_param params[:page]&.to_i || 1 end def per_page_param [params[:per_page]&.to_i || 25, 100].min end end ``` Concerns sind nützlich für wirklich gemeinsam genutzten Code. Vermeiden, Concerns nur zu erstellen, um ein Model zu "verkürzen": Das versteckt Komplexität, ohne sie zu reduzieren. ## Testen mit RSpec ### Frage 10: Wie strukturiert man RSpec-Tests in Rails? RSpec ist das Standard-Testing-Framework für Rails. Eine gute Teststruktur umfasst Model Specs, Controller Specs, Service Specs und Integrationstests. ```ruby # spec/models/user_spec.rb require 'rails_helper' RSpec.describe User, type: :model do # Factories mit FactoryBot let(:user) { build(:user) } let(:admin) { build(:user, :admin) } describe 'validations' do it { is_expected.to validate_presence_of(:email) } it { is_expected.to validate_uniqueness_of(:email).case_insensitive } it 'validiert das E-Mail-Format' do user.email = 'invalid' expect(user).not_to be_valid expect(user.errors[:email]).to include('is invalid') end end describe 'associations' do it { is_expected.to have_many(:articles).dependent(:destroy) } it { is_expected.to have_one(:profile) } it { is_expected.to belong_to(:organization).optional } end describe '#full_name' do it 'gibt Vor- und Nachname kombiniert zurück' do user = build(:user, first_name: 'John', last_name: 'Doe') expect(user.full_name).to eq('John Doe') end it 'behandelt fehlenden Nachnamen' do user = build(:user, first_name: 'John', last_name: nil) expect(user.full_name).to eq('John') end end describe '.active' do it 'gibt nur aktive Benutzer zurück' do active = create(:user, active: true) inactive = create(:user, active: false) expect(User.active).to include(active) expect(User.active).not_to include(inactive) end end end ``` ```ruby # spec/services/order_processor_spec.rb require 'rails_helper' RSpec.describe OrderProcessor do let(:user) { create(:user) } let(:product) { create(:product, stock_quantity: 10, price: 100) } let(:order) { create(:order, user: user, items: [build(:order_item, product: product, quantity: 2)]) } subject { described_class.new(order, payment_method: 'card') } describe '#call' do context 'wenn die Bestellung gültig ist' do before do allow(PaymentGateway).to receive(:charge).and_return( OpenStruct.new(success?: true, transaction_id: 'txn_123') ) end it 'verarbeitet die Bestellung erfolgreich' do result = subject.call expect(result).to be_success expect(order.reload.status).to eq('completed') end it 'verringert den Produktbestand' do expect { subject.call }.to change { product.reload.stock_quantity }.by(-2) end it 'sendet die Bestätigungs-E-Mail' do expect { subject.call } .to have_enqueued_mail(OrderMailer, :confirmation) .with(order) end end context 'wenn die Zahlung fehlschlägt' do before do allow(PaymentGateway).to receive(:charge).and_return( OpenStruct.new(success?: false, error: 'Card declined') ) end it 'gibt ein Fehlerergebnis zurück' do result = subject.call expect(result).to be_failure expect(result.error).to include('Card declined') end it 'aktualisiert den Bestellstatus nicht' do expect { subject.call }.not_to change { order.reload.status } end end end end ``` ```ruby # spec/requests/api/v1/products_spec.rb require 'rails_helper' RSpec.describe 'API V1 Products', type: :request do let(:user) { create(:user) } let(:headers) { { 'Authorization' => "Bearer #{user.api_token}" } } describe 'GET /api/v1/products' do let!(:products) { create_list(:product, 3, :active) } it 'gibt die Produktliste zurück' do get '/api/v1/products', headers: headers expect(response).to have_http_status(:ok) expect(json_response['data'].size).to eq(3) end it 'filtert nach Kategorie' do category = create(:category) categorized = create(:product, category: category) get '/api/v1/products', params: { category_id: category.id }, headers: headers expect(json_response['data'].map { |p| p['id'] }).to eq([categorized.id]) end end describe 'POST /api/v1/products' do let(:valid_params) do { product: { name: 'Neues Produkt', price: 99.99, category_id: create(:category).id } } end it 'erstellt ein neues Produkt' do expect { post '/api/v1/products', params: valid_params, headers: headers }.to change(Product, :count).by(1) expect(response).to have_http_status(:created) end end end ``` Best Practices: `let` für Daten, `describe` für Methoden/Kontexte, `context` für Bedingungen und `it` für spezifische Assertions. Ein Test sollte eine Sache testen. ### Frage 11: Wie verwendet man Factories mit FactoryBot? FactoryBot ermöglicht es, Testdaten deklarativ und wartbar zu erstellen. Factories ersetzen statische Fixtures. ```ruby # spec/factories/users.rb FactoryBot.define do factory :user do # Sequenzen für Eindeutigkeit sequence(:email) { |n| "user#{n}@example.com" } first_name { Faker::Name.first_name } last_name { Faker::Name.last_name } password { 'password123' } confirmed_at { Time.current } # Traits für Variationen trait :admin do role { 'admin' } after(:create) do |user| user.permissions.create!(name: 'admin_access') end end trait :unconfirmed do confirmed_at { nil } end trait :with_profile do after(:create) do |user| create(:profile, user: user) end end trait :with_articles do transient do articles_count { 3 } end after(:create) do |user, evaluator| create_list(:article, evaluator.articles_count, author: user) end end # Geerbte Factory factory :admin_user do admin with_profile end end end ``` ```ruby # spec/factories/orders.rb FactoryBot.define do factory :order do user status { 'pending' } trait :with_items do transient do items_count { 2 } end after(:create) do |order, evaluator| create_list(:order_item, evaluator.items_count, order: order) order.recalculate_total! end end trait :completed do status { 'completed' } processed_at { Time.current } with_items end trait :high_value do after(:create) do |order| create(:order_item, order: order, quantity: 10, unit_price: 500) order.recalculate_total! end end end end ``` ```ruby # Verwendung in Tests RSpec.describe OrderProcessor do # build: nicht persistierte Instanz let(:user) { build(:user) } # create: in DB persistiert let(:order) { create(:order, :with_items, user: user) } # create_list: mehrere Instanzen let(:products) { create_list(:product, 5) } # Traits kombinieren let(:admin) { create(:user, :admin, :with_profile) } # Attribute überschreiben let(:expensive_order) { create(:order, :with_items, items_count: 10) } # build_stubbed: schneller, für Unit-Tests let(:stubbed_user) { build_stubbed(:user) } end ``` `build` oder `build_stubbed` gegenüber `create` bevorzugen, wenn keine Persistenz nötig ist: Das beschleunigt die Tests deutlich. ## Background Jobs ### Frage 12: Wie verwendet man Active Job und Sidekiq in Rails? Active Job bietet eine einheitliche Schnittstelle für Hintergrundjobs, unabhängig vom Backend (Sidekiq, Resque usw.). Sidekiq ist die populäre Wahl wegen seiner Performance mit Redis. ```ruby # app/jobs/process_order_job.rb class ProcessOrderJob < ApplicationJob queue_as :default # Wiederholungskonfiguration retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3 retry_on Net::OpenTimeout, wait: :polynomially_longer, attempts: 10 discard_on ActiveJob::DeserializationError # Sidekiq-Optionen (bei Sidekiq-Backend) sidekiq_options retry: 5, backtrace: true def perform(order_id) order = Order.find(order_id) OrderProcessor.new(order).call rescue ActiveRecord::RecordNotFound # Bestellung zwischen Einreihen und Ausführung gelöscht Rails.logger.warn("Order #{order_id} not found, skipping job") end end ``` ```ruby # app/jobs/batch_email_job.rb class BatchEmailJob < ApplicationJob queue_as :mailers # Rate Limiting mit Sidekiq Enterprise oder Throttle-Gem sidekiq_options throttle: { threshold: 100, period: 1.minute } def perform(user_ids, template_id) template = EmailTemplate.find(template_id) User.where(id: user_ids).find_each do |user| UserMailer.custom_email(user, template).deliver_later end end end ``` ```ruby # Jobs einreihen # Sofort ProcessOrderJob.perform_later(order.id) # Verzögert ProcessOrderJob.set(wait: 5.minutes).perform_later(order.id) # Zu einem bestimmten Zeitpunkt ProcessOrderJob.set(wait_until: Date.tomorrow.noon).perform_later(order.id) # Bestimmte Queue ProcessOrderJob.set(queue: :critical).perform_later(order.id) # Synchron (für Tests oder Debugging) ProcessOrderJob.perform_now(order.id) ``` ```ruby # config/sidekiq.yml :concurrency: 10 :queues: - [critical, 3] # Hohe Priorität, Gewicht 3 - [default, 2] # Mittlere Priorität, Gewicht 2 - [mailers, 1] # Niedrige Priorität, Gewicht 1 - [low, 1] :schedule: cleanup_job: cron: '0 3 * * *' # Jeden Tag um 3 Uhr class: CleanupJob ``` Active Job abstrahiert das Backend, aber der Zugriff auf spezifische Funktionen (Batches, Rate Limiting) erfordert oft eine Kopplung an das gewählte Backend. ## API-Entwicklung ### Frage 13: Wie baut man eine RESTful API mit Rails? Rails erleichtert den Bau von JSON-APIs mit API-only-Controllern und Serializern. Eine gute API ist versioniert, dokumentiert und sicher. ```ruby # app/controllers/api/v1/base_controller.rb module Api module V1 class BaseController < ActionController::API include ActionController::HttpAuthentication::Token::ControllerMethods before_action :authenticate_token! rescue_from ActiveRecord::RecordNotFound, with: :not_found rescue_from ActiveRecord::RecordInvalid, with: :unprocessable_entity rescue_from ActionController::ParameterMissing, with: :bad_request private def authenticate_token! authenticate_or_request_with_http_token do |token, options| @current_user = User.find_by(api_token: token) end end def current_user @current_user end def not_found(exception) render json: { error: 'Ressource nicht gefunden', details: exception.message }, status: :not_found end def unprocessable_entity(exception) render json: { error: 'Validierung fehlgeschlagen', details: exception.record.errors }, status: :unprocessable_entity end def bad_request(exception) render json: { error: 'Ungültige Anfrage', details: exception.message }, status: :bad_request end end end end ``` ```ruby # app/controllers/api/v1/products_controller.rb module Api module V1 class ProductsController < BaseController before_action :set_product, only: [:show, :update, :destroy] def index @products = Product.active .includes(:category) .page(params[:page]) .per(params[:per_page] || 20) render json: { data: ProductSerializer.new(@products).serializable_hash, meta: pagination_meta(@products) } end def show render json: ProductSerializer.new(@product, include: [:category, :reviews]) end def create @product = Product.new(product_params) @product.save! render json: ProductSerializer.new(@product), status: :created end def update @product.update!(product_params) render json: ProductSerializer.new(@product) end def destroy @product.destroy! head :no_content end private def set_product @product = Product.find(params[:id]) end def product_params params.require(:product).permit(:name, :description, :price, :category_id) end def pagination_meta(collection) { current_page: collection.current_page, total_pages: collection.total_pages, total_count: collection.total_count } end end end end ``` ```ruby # app/serializers/product_serializer.rb # Mit der jsonapi-serializer-Gem class ProductSerializer include JSONAPI::Serializer attributes :id, :name, :description, :price, :created_at attribute :formatted_price do |product| "$#{product.price.to_f.round(2)}" end belongs_to :category has_many :reviews link :self do |product| Rails.application.routes.url_helpers.api_v1_product_url(product) end end ``` API-Best-Practices: über Namespace versionieren, passende HTTP-Codes verwenden, Sammlungen paginieren und klare Fehlermeldungen bereitstellen. ### Frage 14: Wie implementiert man JWT-Authentifizierung in Rails? JWT (JSON Web Tokens) ist eine populäre stateless Authentifizierungsmethode für APIs. Das Token kodiert die Identität und Gültigkeit des Benutzers. ```ruby # app/services/jwt_service.rb class JwtService SECRET_KEY = Rails.application.credentials.secret_key_base ALGORITHM = 'HS256'.freeze class << self def encode(payload, exp = 24.hours.from_now) payload[:exp] = exp.to_i payload[:iat] = Time.current.to_i JWT.encode(payload, SECRET_KEY, ALGORITHM) end def decode(token) decoded = JWT.decode(token, SECRET_KEY, true, algorithm: ALGORITHM) HashWithIndifferentAccess.new(decoded.first) rescue JWT::ExpiredSignature raise AuthenticationError, 'Token abgelaufen' rescue JWT::DecodeError raise AuthenticationError, 'Ungültiges Token' end end end ``` ```ruby # app/controllers/api/v1/auth_controller.rb module Api module V1 class AuthController < ActionController::API def login user = User.find_by(email: params[:email]) if user&.authenticate(params[:password]) token = JwtService.encode(user_id: user.id) render json: { token: token, user: UserSerializer.new(user), expires_at: 24.hours.from_now } else render json: { error: 'Ungültige Zugangsdaten' }, status: :unauthorized end end def refresh token = JwtService.encode(user_id: current_user.id) render json: { token: token, expires_at: 24.hours.from_now } end end end end ``` ```ruby # app/controllers/concerns/jwt_authenticatable.rb module JwtAuthenticatable extend ActiveSupport::Concern included do before_action :authenticate_jwt! end private def authenticate_jwt! header = request.headers['Authorization'] token = header&.split(' ')&.last raise AuthenticationError, 'Token fehlt' unless token decoded = JwtService.decode(token) @current_user = User.find(decoded[:user_id]) rescue AuthenticationError => e render json: { error: e.message }, status: :unauthorized rescue ActiveRecord::RecordNotFound render json: { error: 'Benutzer nicht gefunden' }, status: :unauthorized end def current_user @current_user end end ``` Für die Produktion in Betracht ziehen: Refresh Tokens, Token-Blacklisting beim Logout und kurze Ablaufzeiten. Gems wie `devise-jwt` vereinfachen die Implementierung. ## Caching und Performance ### Frage 15: Wie implementiert man Caching in Rails? Rails bietet mehrere Caching-Ebenen: Fragment Caching, Russian Doll Caching, Low-Level-Caching. Die Wahl hängt vom Anwendungsfall ab. ```ruby # config/environments/production.rb config.action_controller.perform_caching = true config.cache_store = :redis_cache_store, { url: ENV['REDIS_URL'], namespace: 'myapp:cache', expires_in: 1.day, race_condition_ttl: 10.seconds } ``` ```erb <%# app/views/products/index.html.erb %> <%# Fragment Caching mit automatischem Cache-Schlüssel %> <% @products.each do |product| %> <%# Cache basiert auf updated_at des Produkts %> <% cache product do %> <%= render product %> <% end %> <% end %> <%# Russian Doll Caching - verschachtelter Cache %> <% cache ['v1', @category] do %>

<%= @category.name %>

<% @category.products.each do |product| %> <% cache ['v1', product] do %> <%= render product %> <% end %> <% end %> <% end %> <%# Bedingtes Caching %> <% cache_if current_user.nil?, @product do %> <%= render @product %> <% end %> ``` ```ruby # app/models/product.rb class Product < ApplicationRecord # Touch des Parents zum Invalidieren des Russian-Doll-Caches belongs_to :category, touch: true # Benutzerdefinierter Cache-Schlüssel def cache_key_with_version "#{super}/#{reviews.maximum(:updated_at)&.to_i}" end end ``` ```ruby # Low-Level-Caching in Services class DashboardStatsService def call Rails.cache.fetch('dashboard:stats', expires_in: 15.minutes) do { total_users: User.count, active_users: User.where('last_sign_in_at > ?', 30.days.ago).count, total_orders: Order.completed.count, revenue_mtd: Order.completed.where(created_at: Time.current.beginning_of_month..).sum(:total) } end end end # Cache mit Schutz vor Race Conditions Rails.cache.fetch('popular_products', expires_in: 1.hour, race_condition_ttl: 10.seconds) do Product.bestsellers.limit(10).to_a end # Explizite Invalidierung Rails.cache.delete('dashboard:stats') Rails.cache.delete_matched('products:*') ``` Russian Doll Caching ist effektiv, weil nur geänderte Fragmente neu generiert werden. `touch: true` auf Assoziationen verwenden, um die Invalidierung zu propagieren. ### Frage 16: Wie optimiert man die Performance einer Rails-Anwendung? Die Rails-Optimierung umfasst mehrere Aspekte: DB-Abfragen, Caching, Assets und Architektur. Ein methodisches Vorgehen mit Monitoring ist essenziell. ```ruby # Datenbank-Optimierung # config/database.yml production: pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> prepared_statements: true advisory_locks: true # app/models/order.rb class Order < ApplicationRecord # Zusammengesetzte Indizes für häufige Abfragen # add_index :orders, [:user_id, :status, :created_at] # Nur die benötigten Spalten auswählen scope :summary, -> { select(:id, :status, :total, :created_at) } # Stapelverarbeitung für große Datenmengen def self.process_pending pending.find_each(batch_size: 1000) do |order| ProcessOrderJob.perform_later(order.id) end end # Wiederkehrende Berechnungen vermeiden def self.revenue_by_month completed .group("DATE_TRUNC('month', created_at)") .sum(:total) end end ``` ```ruby # Speicheroptimierung # config/puma.rb workers ENV.fetch("WEB_CONCURRENCY") { 2 } threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } threads threads_count, threads_count preload_app! before_fork do ActiveRecord::Base.connection_pool.disconnect! end on_worker_boot do ActiveRecord::Base.establish_connection end ``` ```ruby # Profiling mit rack-mini-profiler # Gemfile group :development do gem 'rack-mini-profiler' gem 'memory_profiler' gem 'stackprof' end # config/initializers/mini_profiler.rb if defined?(Rack::MiniProfiler) Rack::MiniProfiler.config.position = 'bottom-right' Rack::MiniProfiler.config.start_hidden = true end ``` ```ruby # Lazy Loading und Pagination class ProductsController < ApplicationController def index @products = Product.active .includes(:category, :primary_image) .page(params[:page]) .per(24) # Prefetch für die nächste Seite if @products.next_page Rails.cache.fetch("products:page:#{@products.next_page}", expires_in: 5.minutes) do Product.active.page(@products.next_page).per(24).to_a end end end end ``` Wesentliche Tools: `rack-mini-profiler` für Profiling, `bullet` für N+1-Erkennung, `New Relic` oder `Scout` für Produktions-Monitoring. ## Sicherheit ### Frage 17: Was sind die Best Practices für Sicherheit in Rails? Rails enthält Standardschutzmechanismen gegen gängige Schwachstellen. Diese Schutzmechanismen zu verstehen und korrekt zu konfigurieren ist entscheidend. ```ruby # CSRF-Schutz # app/controllers/application_controller.rb class ApplicationController < ActionController::Base # Standardmäßig aktiviert, löst Exception aus, wenn das Token ungültig ist protect_from_forgery with: :exception # Für APIs :null_session verwenden # protect_from_forgery with: :null_session end # In Views ist das Token in Formularen automatisch enthalten # <%= form_with ... %> beinhaltet authenticity_token # Für AJAX-Anfragen # Header X-CSRF-Token mit dem Wert von csrf_meta_tags hinzufügen ``` ```ruby # Schutz vor SQL-Injection # ✅ Interpolierte Parameter werden automatisch escaped User.where('email = ?', params[:email]) User.where(email: params[:email]) # ❌ GEFAHR - Direkte Interpolation User.where("email = '#{params[:email]}'") # ✅ Für dynamische ORDER-Klauseln ALLOWED_SORTS = %w[name created_at price].freeze sort_column = ALLOWED_SORTS.include?(params[:sort]) ? params[:sort] : 'name' Product.order(sort_column) ``` ```ruby # XSS-Schutz # Rails escaped HTML in Views automatisch # ✅ Automatisch escaped <%= user.name %> # ❌ Gefährlich - nicht escapeter Inhalt <%== user.bio %> <%= raw user.bio %> <%= user.bio.html_safe %> # ✅ Für sicheres HTML sanitize verwenden <%= sanitize user.bio, tags: %w[p br strong em] %> ``` ```ruby # Strong Parameters class UsersController < ApplicationController def update @user.update!(user_params) end private def user_params # Explizite Whitelist erlaubter Attribute params.require(:user).permit(:name, :email, :avatar) # Nur für Administratoren if current_user.admin? params.require(:user).permit(:name, :email, :role, :active) else params.require(:user).permit(:name, :email) end end end ``` ```ruby # Sicherheits-Header # config/initializers/secure_headers.rb Rails.application.config.action_dispatch.default_headers = { 'X-Frame-Options' => 'SAMEORIGIN', 'X-XSS-Protection' => '1; mode=block', 'X-Content-Type-Options' => 'nosniff', 'X-Download-Options' => 'noopen', 'X-Permitted-Cross-Domain-Policies' => 'none', 'Referrer-Policy' => 'strict-origin-when-cross-origin' } # Content Security Policy Rails.application.config.content_security_policy do |policy| policy.default_src :self policy.script_src :self policy.style_src :self, :unsafe_inline policy.img_src :self, :data, 'https:' end ``` Regelmäßig mit `brakeman` (statische Sicherheitsanalyse) auditieren und Gems mit `bundle audit` aktuell halten. ### Frage 18: Wie behandelt man Authentifizierung und Autorisierung in Rails? Authentifizierung verifiziert die Identität, Autorisierung steuert die Berechtigungen. Devise verwaltet die Auth, Pundit oder CanCanCan verwalten die Autorisierung. ```ruby # Devise-Setup # app/models/user.rb class User < ApplicationRecord devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable, :confirmable, :lockable, :trackable enum role: { user: 0, moderator: 1, admin: 2 } def admin? role == 'admin' end end ``` ```ruby # Pundit-Policies # app/policies/article_policy.rb class ArticlePolicy < ApplicationPolicy def index? true end def show? record.published? || owner_or_admin? end def create? user.present? end def update? owner_or_admin? end def destroy? owner_or_admin? end def publish? user&.admin? || user&.moderator? end # Scope für Sammlungen class Scope < Scope def resolve if user&.admin? scope.all elsif user scope.where(published: true).or(scope.where(author: user)) else scope.where(published: true) end end end private def owner_or_admin? user&.admin? || record.author == user end end ``` ```ruby # Controller mit Pundit class ArticlesController < ApplicationController include Pundit::Authorization after_action :verify_authorized, except: :index after_action :verify_policy_scoped, only: :index def index @articles = policy_scope(Article).includes(:author).page(params[:page]) end def show @article = Article.find(params[:id]) authorize @article end def update @article = Article.find(params[:id]) authorize @article if @article.update(article_params) redirect_to @article, notice: 'Artikel aktualisiert.' else render :edit, status: :unprocessable_entity end end def publish @article = Article.find(params[:id]) authorize @article @article.update!(published: true, published_at: Time.current) redirect_to @article, notice: 'Artikel veröffentlicht.' end rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized private def user_not_authorized flash[:alert] = "Sie sind nicht berechtigt, diese Aktion auszuführen." redirect_back(fallback_location: root_path) end end ``` Pundit ist expliziter und besser testbar als CanCanCan. Jede Aktion hat eine entsprechende Policy-Methode, und Scopes filtern Sammlungen automatisch. ## Fortgeschrittenes Rails ### Frage 19: Erklären Sie das Repository-Pattern in Rails Das Repository-Pattern isoliert die Datenzugriffslogik vom Rest der Anwendung. Auch wenn Rails Active Record (ein anderes Pattern) verwendet, kann Repository für komplexe Fälle nützlich sein. ```ruby # app/repositories/base_repository.rb class BaseRepository def initialize(model_class) @model_class = model_class end def all @model_class.all end def find(id) @model_class.find(id) end def find_by(attributes) @model_class.find_by(attributes) end def create(attributes) @model_class.create(attributes) end def update(record, attributes) record.update(attributes) end def delete(record) record.destroy end end ``` ```ruby # app/repositories/product_repository.rb class ProductRepository < BaseRepository def initialize super(Product) end def active @model_class.where(active: true) end def in_category(category_id) @model_class.where(category_id: category_id) end def search(query) @model_class.where('name ILIKE ? OR description ILIKE ?', "%#{query}%", "%#{query}%") end def with_stock @model_class.where('stock_quantity > 0') end def bestsellers(limit: 10) @model_class .joins(:order_items) .group(:id) .order('COUNT(order_items.id) DESC') .limit(limit) end def for_homepage active .with_stock .includes(:category, :primary_image) .order(featured: :desc, created_at: :desc) .limit(12) end end ``` ```ruby # Verwendung in einem Service class ProductSearchService def initialize(repository: ProductRepository.new) @repository = repository end def call(params) products = @repository.active products = products.in_category(params[:category]) if params[:category] products = products.search(params[:query]) if params[:query].present? products = products.with_stock if params[:in_stock] products end end # Erleichtert das Testen mit Mocks RSpec.describe ProductSearchService do let(:repository) { instance_double(ProductRepository) } let(:service) { described_class.new(repository: repository) } it 'filtert nach Kategorie' do products = double('products') allow(repository).to receive(:active).and_return(products) allow(products).to receive(:in_category).with(1).and_return(products) service.call(category: 1) expect(products).to have_received(:in_category).with(1) end end ``` Repository ist in Rails optional, da Active Record bereits ein hervorragendes Pattern ist. Für komplexe Abfragen verwenden oder wenn Storage-Isolation wichtig ist. ### Frage 20: Wie implementiert man das CQRS-Pattern in Rails? CQRS (Command Query Responsibility Segregation) trennt Lese- und Schreiboperationen. In Rails übersetzt sich das in getrennte Klassen für Queries und Commands. ```ruby # app/commands/base_command.rb class BaseCommand include ActiveModel::Validations def self.call(*args) new(*args).call end def call return failure(errors) unless valid? execute end private def execute raise NotImplementedError end def success(data = nil) CommandResult.success(data) end def failure(errors) CommandResult.failure(errors) end end CommandResult = Struct.new(:success, :data, :errors, keyword_init: true) do def success? = success def failure? = !success def self.success(data) new(success: true, data: data, errors: []) end def self.failure(errors) new(success: false, data: nil, errors: Array(errors)) end end ``` ```ruby # app/commands/orders/create_order_command.rb module Orders class CreateOrderCommand < BaseCommand attr_reader :user, :items, :shipping_address validates :user, presence: true validates :items, presence: true validate :validate_items_availability def initialize(user:, items:, shipping_address:) @user = user @items = items @shipping_address = shipping_address end private def execute order = nil ActiveRecord::Base.transaction do order = Order.create!( user: user, shipping_address: shipping_address, status: 'pending' ) items.each do |item| order.items.create!( product_id: item[:product_id], quantity: item[:quantity], unit_price: Product.find(item[:product_id]).price ) end order.calculate_total! end OrderCreatedEvent.broadcast(order) success(order) rescue ActiveRecord::RecordInvalid => e failure(e.message) end def validate_items_availability items.each do |item| product = Product.find_by(id: item[:product_id]) unless product&.stock_quantity&.>= item[:quantity] errors.add(:items, "Produkt #{item[:product_id]} nicht verfügbar") end end end end end ``` ```ruby # app/queries/orders/user_orders_query.rb module Orders class UserOrdersQuery def initialize(user, params = {}) @user = user @params = params end def call orders = @user.orders.includes(:items, items: :product) orders = apply_status_filter(orders) orders = apply_date_filter(orders) orders = apply_sorting(orders) orders.page(@params[:page]).per(@params[:per_page] || 20) end private def apply_status_filter(orders) return orders unless @params[:status] orders.where(status: @params[:status]) end def apply_date_filter(orders) orders = orders.where('created_at >= ?', @params[:from]) if @params[:from] orders = orders.where('created_at <= ?', @params[:to]) if @params[:to] orders end def apply_sorting(orders) case @params[:sort] when 'oldest' then orders.order(created_at: :asc) when 'total_desc' then orders.order(total: :desc) else orders.order(created_at: :desc) end end end end ``` ```ruby # Controller mit CQRS class OrdersController < ApplicationController def index @orders = Orders::UserOrdersQuery.new(current_user, filter_params).call end def create result = Orders::CreateOrderCommand.call( user: current_user, items: order_params[:items], shipping_address: order_params[:shipping_address] ) if result.success? redirect_to result.data, notice: 'Bestellung erstellt!' else flash.now[:alert] = result.errors.join(', ') render :new, status: :unprocessable_entity end end end ``` CQRS glänzt bei komplexen Anwendungen mit asymmetrischen Lese-/Schreibanforderungen. Für einfaches CRUD ist es Over-Engineering. ### Frage 21: Wie behandelt man WebSockets mit Action Cable? Action Cable integriert WebSockets in Rails für bidirektionale Echtzeitkommunikation. Es nutzt Redis zur Synchronisation zwischen Servern. ```ruby # app/channels/application_cable/connection.rb module ApplicationCable class Connection < ActionCable::Connection::Base identified_by :current_user def connect self.current_user = find_verified_user end private def find_verified_user # Über Session-Cookie if verified_user = User.find_by(id: cookies.encrypted[:user_id]) verified_user # Über JWT für APIs elsif verified_user = verify_jwt_token verified_user else reject_unauthorized_connection end end def verify_jwt_token token = request.params[:token] return nil unless token decoded = JwtService.decode(token) User.find(decoded[:user_id]) rescue nil end end end ``` ```ruby # app/channels/chat_channel.rb class ChatChannel < ApplicationCable::Channel def subscribed @room = ChatRoom.find(params[:room_id]) # Berechtigungen prüfen unless @room.accessible_by?(current_user) reject return end stream_for @room # Andere über die Anwesenheit informieren broadcast_presence(:joined) end def unsubscribed broadcast_presence(:left) if @room end def send_message(data) message = @room.messages.create!( user: current_user, content: data['content'] ) # An alle Abonnenten senden ChatChannel.broadcast_to(@room, { type: 'message', message: MessageSerializer.new(message).as_json }) end def typing ChatChannel.broadcast_to(@room, { type: 'typing', user: current_user.name }) end private def broadcast_presence(action) ChatChannel.broadcast_to(@room, { type: 'presence', action: action, user: current_user.name, online_count: @room.online_users_count }) end end ``` ```javascript // app/javascript/channels/chat_channel.js import consumer from "./consumer" const chatChannel = consumer.subscriptions.create( { channel: "ChatChannel", room_id: roomId }, { connected() { console.log("Connected to chat") }, disconnected() { console.log("Disconnected from chat") }, received(data) { switch(data.type) { case 'message': this.appendMessage(data.message) break case 'typing': this.showTypingIndicator(data.user) break case 'presence': this.updatePresence(data) break } }, sendMessage(content) { this.perform('send_message', { content: content }) }, notifyTyping() { this.perform('typing') } } ) ``` Action Cable behandelt Reconnects und Synchronisation automatisch. In der Produktion Redis als Adapter konfigurieren und entsprechend der gleichzeitigen Verbindungen skalieren. ### Frage 22: Wie implementiert man Multi-Tenancy in Rails? Multi-Tenancy ermöglicht es einer Anwendung, mehrere isolierte Kunden (Tenants) zu bedienen. Drei Hauptansätze: auf Datenbankebene, auf Schemaebene oder auf Zeilenebene. ```ruby # Multi-Tenancy auf Zeilenebene mit ActsAsTenant oder manuell # app/models/concerns/tenant_scoped.rb module TenantScoped extend ActiveSupport::Concern included do belongs_to :tenant # Standard-Scope auf den aktuellen Tenant default_scope -> { where(tenant: Current.tenant) if Current.tenant } # Tenant-Validierung before_validation :set_tenant, on: :create end private def set_tenant self.tenant ||= Current.tenant end end # app/models/current.rb class Current < ActiveSupport::CurrentAttributes attribute :tenant, :user end ``` ```ruby # app/controllers/application_controller.rb class ApplicationController < ActionController::Base before_action :set_current_tenant private def set_current_tenant Current.tenant = resolve_tenant Current.user = current_user end def resolve_tenant # Über Subdomain if request.subdomain.present? && request.subdomain != 'www' Tenant.find_by!(subdomain: request.subdomain) # Über Header (für APIs) elsif request.headers['X-Tenant-ID'].present? Tenant.find(request.headers['X-Tenant-ID']) # Über Benutzer elsif current_user current_user.tenant end rescue ActiveRecord::RecordNotFound redirect_to root_url(subdomain: 'www'), alert: 'Tenant nicht gefunden' end end ``` ```ruby # app/models/project.rb class Project < ApplicationRecord include TenantScoped has_many :tasks belongs_to :owner, class_name: 'User' end # app/models/user.rb class User < ApplicationRecord include TenantScoped has_many :projects, foreign_key: :owner_id # Administratoren können mehreren Tenants angehören has_many :tenant_memberships has_many :accessible_tenants, through: :tenant_memberships, source: :tenant end ``` ```ruby # Auf Schemaebene mit der Apartment-Gem (PostgreSQL) # config/initializers/apartment.rb Apartment.configure do |config| config.excluded_models = %w[Tenant User] config.tenant_names = -> { Tenant.pluck(:subdomain) } end # Verwendung Apartment::Tenant.switch('acme') do # Alle Abfragen in diesem Block nutzen das 'acme'-Schema Project.all # SELECT * FROM acme.projects end ``` Zeilenebene ist am einfachsten, erfordert aber ständige Aufmerksamkeit für Lecks. Schemaebene bietet bessere Isolation, kompliziert aber Migrationen. Je nach Sicherheits- und Skalierbarkeitsanforderungen wählen. ### Frage 23: Wie richtet man eine Microservices-Architektur mit Rails ein? Rails kann als Basis für eine Microservices-Architektur mit Kommunikation über HTTP/gRPC oder Message Queues dienen. Der Schlüssel liegt in einer guten Definition der Grenzen. ```ruby # HTTP-Service-Client # app/services/payment_service_client.rb class PaymentServiceClient include HTTParty base_uri ENV.fetch('PAYMENT_SERVICE_URL') def initialize @options = { headers: { 'Content-Type' => 'application/json', 'X-Service-Token' => ENV.fetch('SERVICE_TOKEN') }, timeout: 10 } end def create_charge(amount:, currency:, source:, metadata: {}) response = self.class.post('/charges', @options.merge( body: { amount: amount, currency: currency, source: source, metadata: metadata }.to_json )) handle_response(response) end def get_charge(charge_id) response = self.class.get("/charges/#{charge_id}", @options) handle_response(response) end private def handle_response(response) case response.code when 200..299 ServiceResult.success(response.parsed_response) when 400..499 ServiceResult.failure(response.parsed_response['error'], code: response.code) else ServiceResult.failure('Service nicht verfügbar', code: response.code) end rescue Net::OpenTimeout, Net::ReadTimeout ServiceResult.failure('Service-Timeout') end end ``` ```ruby # Event-getriebene Kommunikation mit Sidekiq/Redis # app/events/order_events.rb module OrderEvents class Created include Wisper::Publisher def call(order) broadcast(:order_created, order) end end end # app/listeners/inventory_listener.rb class InventoryListener def order_created(order) order.items.each do |item| InventoryServiceClient.new.reserve_stock( product_id: item.product_id, quantity: item.quantity, reference: order.id ) end end end # config/initializers/wisper.rb Wisper.subscribe(InventoryListener.new, async: true) Wisper.subscribe(NotificationListener.new, async: true) ``` ```ruby # API-Gateway-Pattern # app/controllers/api/v1/gateway_controller.rb module Api module V1 class GatewayController < BaseController # Mehrere Services aggregieren def dashboard results = Parallel.map([:orders, :inventory, :analytics], in_threads: 3) do |service| fetch_from_service(service) end render json: { orders: results[0], inventory: results[1], analytics: results[2] } end private def fetch_from_service(service) case service when :orders OrderServiceClient.new.recent_orders(limit: 5) when :inventory InventoryServiceClient.new.low_stock_alerts when :analytics AnalyticsServiceClient.new.daily_summary end rescue => e { error: "#{service} nicht verfügbar", message: e.message } end end end end ``` Für Rails-Microservices: klare API-Verträge definieren (OpenAPI), Circuit Breaker implementieren (Gem `circuitbox`) und Distributed Tracing nutzen (Gem `opentelemetry`). ### Frage 24: Wie deployt man eine Rails-Anwendung in Produktion? Modernes Rails-Deployment nutzt Container oder PaaS. Eine robuste Produktionskonfiguration deckt Assets, Datenbank und Monitoring ab. ```ruby # config/environments/production.rb Rails.application.configure do config.cache_classes = true config.eager_load = true config.consider_all_requests_local = false # Assets config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? config.assets.compile = false config.assets.digest = true # Logging config.log_level = ENV.fetch('LOG_LEVEL', 'info').to_sym config.log_tags = [:request_id] config.logger = ActiveSupport::Logger.new(STDOUT) .tap { |logger| logger.formatter = Logger::Formatter.new } .then { |logger| ActiveSupport::TaggedLogging.new(logger) } # Cache config.cache_store = :redis_cache_store, { url: ENV['REDIS_URL'], expires_in: 1.day } # SSL erzwingen config.force_ssl = true config.ssl_options = { hsts: { subdomains: true } } # Action Mailer config.action_mailer.delivery_method = :smtp config.action_mailer.smtp_settings = { address: ENV['SMTP_HOST'], port: ENV['SMTP_PORT'], user_name: ENV['SMTP_USER'], password: ENV['SMTP_PASSWORD'], authentication: :plain, enable_starttls_auto: true } end ``` ```dockerfile # Dockerfile FROM ruby:3.3-alpine AS builder RUN apk add --no-cache build-base postgresql-dev nodejs yarn WORKDIR /app COPY Gemfile Gemfile.lock ./ RUN bundle config set --local deployment true && \ bundle config set --local without 'development test' && \ bundle install COPY package.json yarn.lock ./ RUN yarn install --frozen-lockfile COPY . . RUN bundle exec rails assets:precompile # Produktions-Image FROM ruby:3.3-alpine RUN apk add --no-cache postgresql-client tzdata WORKDIR /app COPY --from=builder /app /app COPY --from=builder /usr/local/bundle /usr/local/bundle ENV RAILS_ENV=production ENV RAILS_LOG_TO_STDOUT=true EXPOSE 3000 CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"] ``` ```yaml # docker-compose.production.yml version: '3.8' services: web: build: . environment: - DATABASE_URL=postgres://user:pass@db/app_production - REDIS_URL=redis://redis:6379/0 - SECRET_KEY_BASE=${SECRET_KEY_BASE} depends_on: - db - redis deploy: replicas: 3 resources: limits: memory: 512M sidekiq: build: . command: bundle exec sidekiq environment: - DATABASE_URL=postgres://user:pass@db/app_production - REDIS_URL=redis://redis:6379/0 depends_on: - db - redis deploy: replicas: 2 db: image: postgres:15-alpine volumes: - postgres_data:/var/lib/postgresql/data environment: - POSTGRES_PASSWORD=${DB_PASSWORD} redis: image: redis:7-alpine volumes: - redis_data:/data volumes: postgres_data: redis_data: ``` Produktions-Checkliste: SSL verpflichtend, Secrets über ENV, Health Checks, automatisierte DB-Backups, Monitoring (APM + Logs + Metriken) und konfiguriertes Alerting. ### Frage 25: Was sind die neuen Features in Rails 7+, die man kennen sollte? Rails 7+ bringt bedeutende Änderungen: Hotwire standardmäßig, Import Maps, verbesserte verschlüsselte Credentials und viele Optimierungen. ```ruby # Hotwire - Turbo Frames # app/views/articles/index.html.erb <%= turbo_frame_tag "articles" do %> <% @articles.each do |article| %> <%= turbo_frame_tag dom_id(article) do %> <%= render article %> <% end %> <% end %> <%= link_to "Mehr laden", articles_path(page: @page + 1), data: { turbo_frame: "articles" } %> <% end %> # Turbo Streams für Echtzeit-Updates # app/controllers/comments_controller.rb def create @comment = @article.comments.create!(comment_params.merge(user: current_user)) respond_to do |format| format.turbo_stream format.html { redirect_to @article } end end # app/views/comments/create.turbo_stream.erb <%= turbo_stream.append "comments", @comment %> <%= turbo_stream.update "comments_count", @article.comments.count %> <%= turbo_stream.replace "comment_form", partial: "comments/form", locals: { comment: Comment.new } %> ``` ```ruby # Stimulus-Controller # app/javascript/controllers/search_controller.js import { Controller } from "@hotwired/stimulus" import { debounce } from "lodash-es" export default class extends Controller { static targets = ["input", "results"] static values = { url: String } connect() { this.search = debounce(this.search.bind(this), 300) } async search() { const query = this.inputTarget.value if (query.length < 2) return const response = await fetch(`${this.urlValue}?q=${encodeURIComponent(query)}`) this.resultsTarget.innerHTML = await response.text() } } ``` ```ruby # Import Maps (ohne JavaScript-Bundler) # config/importmap.rb pin "application" pin "@hotwired/turbo-rails", to: "turbo.min.js" pin "@hotwired/stimulus", to: "stimulus.min.js" pin "@hotwired/stimulus-loading", to: "stimulus-loading.js" pin_all_from "app/javascript/controllers", under: "controllers" # Pins vom CDN pin "lodash-es", to: "https://ga.jspm.io/npm:lodash-es@4.17.21/lodash.js" ``` ```ruby # Active Record Encryption (Rails 7+) # app/models/user.rb class User < ApplicationRecord encrypts :email, deterministic: true # Erlaubt Suchen encrypts :phone_number # Standardmäßig nicht-deterministisch encrypts :ssn, deterministic: true, downcase: true end # config/credentials.yml.enc active_record_encryption: primary_key: abc123... deterministic_key: def456... key_derivation_salt: ghi789... ``` ```ruby # Verbesserungen an der Query-Schnittstelle # Rails 7.1+ # Asynchrone Queries users = User.where(active: true).load_async # Weiterarbeiten, während die Query läuft # Auf Ergebnisse mit users.to_a zugreifen # Common Table Expressions (CTE) User.with( recent_orders: Order.where('created_at > ?', 30.days.ago) ).joins('JOIN recent_orders ON recent_orders.user_id = users.id') # Automatische inverse_of-Erkennung class Author < ApplicationRecord has_many :books # inverse_of automatisch erkannt end # Strict Loading standardmäßig (vermeidet N+1) class ApplicationRecord < ActiveRecord::Base self.strict_loading_by_default = true end ``` Rails 7+ bevorzugt Einfachheit (kein Webpack standardmäßig) und HTML-over-the-wire mit Hotwire. Dieser Ansatz reduziert die JavaScript-Komplexität und bietet gleichzeitig eine moderne Benutzererfahrung. ## Fazit Ruby-on-Rails-Interviews bewerten die Beherrschung des gesamten Frameworks und das Verständnis seiner Konventionen. Wichtige Punkte zum Merken: ✅ **Grundlagen**: MVC, Active Record, Migrationen, Validierungen und Assoziationen ✅ **Architektur**: Service Objects, Concerns, Query Objects und CQRS-Patterns ✅ **Performance**: N+1-Abfragen, Caching (Fragment, Russian Doll, Low-Level), Eager Loading ✅ **Testing**: RSpec, FactoryBot, Request Specs und Best Practices beim Testen ✅ **Sicherheit**: CSRF, SQL-Injection, XSS, Strong Parameters und Authentifizierung/Autorisierung ✅ **APIs**: RESTful-Design, JWT, Serializer und Versionierung ✅ **Produktion**: Background Jobs, WebSockets, Deployment und Monitoring Die Rails-Philosophie (Convention over Configuration, DRY und Rails Way) leitet alle architektonischen Entscheidungen. Diese Prinzipien zu beherrschen und zu wissen, wann von ihnen abgewichen werden sollte, demonstriert solide Expertise. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/de/blog/ruby-on-rails/ruby-on-rails-interview-questions