# Ruby on Rails sollicitatievragen: Top 25 in 2026 > De 25 meest gestelde Ruby on Rails sollicitatievragen. MVC-architectuur, Active Record, migraties, RSpec-testing, REST-APIs met gedetailleerde antwoorden en codevoorbeelden. - Published: 2026-02-03 - Updated: 2026-04-27 - Author: Anthony Fillion-Maillet - Tags: ruby on rails, ruby, sollicitatie, active record, technische sollicitatie - Reading time: 22 min --- Ruby on Rails-sollicitatiegesprekken beoordelen de beheersing van het populairste Ruby-framework, het begrip van de MVC-architectuur, de Active Record-ORM en het vermogen om robuuste webapplicaties te bouwen volgens de filosofie "Convention over Configuration". Deze gids behandelt de 25 meest gestelde vragen, van Rails-grondbeginselen tot geavanceerde productiepatronen. > **Tip voor het sollicitatiegesprek** > > Recruiters waarderen kandidaten die de Rails-filosofie begrijpen: "Convention over Configuration", DRY (Don't Repeat Yourself) en Rails Way-patronen. Uitleggen waarom Rails bepaalde architecturale keuzes maakt, maakt het verschil. ## Ruby on Rails-grondbeginselen ### Vraag 1: Leg het MVC-patroon in Ruby on Rails uit Het Model-View-Controller-patroon (MVC) vormt de architecturale kern van Rails. Het scheidt verantwoordelijkheden in drie afzonderlijke lagen voor betere onderhoudbaarheid en testbaarheid van de code. ```ruby # app/models/article.rb # Het Model beheert de gegevens en de bedrijfslogica class Article < ApplicationRecord # Datavalidaties validates :title, presence: true, length: { minimum: 5 } validates :body, presence: true # Associaties met andere modellen belongs_to :author, class_name: 'User' has_many :comments, dependent: :destroy has_many :tags, through: :article_tags # Scopes voor herbruikbare queries scope :published, -> { where(published: true) } scope :recent, -> { order(created_at: :desc).limit(10) } # Levenscyclus-callbacks before_save :generate_slug private def generate_slug self.slug = title.parameterize if title_changed? end end ``` ```ruby # app/controllers/articles_controller.rb # De Controller ontvangt verzoeken en orkestreert het antwoord 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 succesvol aangemaakt.' 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 %> <%# De View toont de gegevens in HTML-formaat %>

<%= @article.title %>

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

<%= simple_format @article.body %>
<%# Partial voor de reacties %> <%= render @comments %>
``` De typische flow: het verzoek komt aan bij de Router, die het naar de juiste Controller stuurt. De Controller communiceert met het Model om gegevens op te halen of te wijzigen, en geeft die gegevens vervolgens door aan de View voor HTML-rendering. ### Vraag 2: Wat is Active Record en hoe werkt de Rails-ORM? Active Record is de ORM (Object-Relational Mapping) van Rails die het Active Record-patroon implementeert. Elke Model-klasse vertegenwoordigt een databasetabel en elke instantie vertegenwoordigt een rij. ```ruby # app/models/user.rb # Active Record koppelt kolommen automatisch aan attributen class User < ApplicationRecord # De tabel 'users' wordt automatisch gekoppeld # Kolommen: id, email, name, created_at, updated_at has_secure_password # BCrypt voor het wachtwoord has_many :articles, foreign_key: :author_id has_one :profile, dependent: :destroy has_and_belongs_to_many :roles # Validaties validates :email, presence: true, uniqueness: { case_sensitive: false }, format: { with: URI::MailTo::EMAIL_REGEXP } # Callbacks before_save :normalize_email # Klassemethoden voor queries def self.admins joins(:roles).where(roles: { name: 'admin' }) end private def normalize_email self.email = email.downcase.strip end end ``` ```ruby # Voorbeelden van Active Record-queries # Rails-console of binnen een service # Aanmaken user = User.create!(email: 'dev@example.com', name: 'Alice', password: 'secret123') # Lezen met voorwaarden active_users = User.where(active: true).order(:name) user = User.find_by(email: 'dev@example.com') # Geketende queries (lazy evaluation) recent_admins = User.admins .where('created_at > ?', 1.month.ago) .includes(:profile) .limit(10) # N+1-preventie met eager loading articles = Article.includes(:author, :comments).published # Bijwerken user.update!(name: 'Alice Martin') # Transacties User.transaction do user.debit_balance!(100) recipient.credit_balance!(100) Payment.create!(from: user, to: recipient, amount: 100) end ``` Active Record vertaalt Ruby-methoden naar geoptimaliseerde SQL-queries. Methoden zoals `where`, `joins` en `includes` zijn lazy: de query wordt pas uitgevoerd bij het itereren of bij het aanroepen van `to_a`. ### Vraag 3: Leg het migratiesysteem van Rails uit Migraties maken het mogelijk om het databaseschema met Ruby te versioneren. Ze zijn omkeerbaar en stellen een gecontroleerde evolutie van de gegevensstructuur mogelijk. ```ruby # db/migrate/20260203100000_create_products.rb # Migratie om een tabel aan te maken 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 en updated_at automatisch end # Indexen voor performance add_index :products, :name add_index :products, [:category_id, :active] end end ``` ```ruby # db/migrate/20260203110000_add_slug_to_products.rb # Migratie om een bestaande tabel te wijzigen class AddSlugToProducts < ActiveRecord::Migration[7.1] def change add_column :products, :slug, :string add_index :products, :slug, unique: true # Bestaande slugs vullen reversible do |dir| dir.up do Product.find_each do |product| product.update_column(:slug, product.name.parameterize) end end end # Niet-nullable maken na het vullen change_column_null :products, :slug, false end end ``` ```bash # Essentiële migratiecommando's rails db:migrate # Openstaande migraties uitvoeren rails db:rollback # Laatste migratie ongedaan maken rails db:rollback STEP=3 # Laatste 3 migraties ongedaan maken rails db:migrate:status # Status van migraties bekijken rails db:seed # db/seeds.rb uitvoeren rails db:reset # Drop, create, migrate, seed ``` Migraties moeten omkeerbaar zijn. De methode `change` is intelligent en kan veelvoorkomende operaties automatisch terugdraaien. Voor complexe gevallen `up` en `down` apart gebruiken. ## Geavanceerd Active Record ### Vraag 4: Hoe N+1-queries optimaliseren in Rails? Het N+1-probleem treedt op wanneer een initiële query gevolgd wordt door N extra queries om associaties te laden. Rails biedt verschillende eager loading-methoden om dit probleem op te lossen. ```ruby # app/controllers/orders_controller.rb class OrdersController < ApplicationController def index # ❌ N+1-PROBLEEM: 1 query + N queries per bestelling # @orders = Order.all # In de view: order.user.name genereert een query per bestelling # ✅ OPLOSSING met includes (eager loading) @orders = Order.includes(:user, :items) .where(status: 'completed') .order(created_at: :desc) # Genereert in totaal slechts 3 queries end def show # includes: laadt associaties apart (2-3 queries) @order = Order.includes(items: :product).find(params[:id]) # preload: forceert apart laden @order = Order.preload(:items, :user).find(params[:id]) # eager_load: forceert LEFT OUTER JOIN (1 query) @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 met standaard includes scope :with_details, -> { includes(:user, items: :product) } # Counter cache om COUNT-queries te vermijden # Vereist: add_column :users, :orders_count, :integer, default: 0 belongs_to :user, counter_cache: true end ``` ```ruby # N+1-detectie met de Bullet-gem (development) # config/environments/development.rb config.after_initialize do Bullet.enable = true Bullet.alert = true Bullet.bullet_logger = true Bullet.rails_logger = true end # Bullet toont waarschuwingen wanneer: # - Een N+1-query wordt gedetecteerd # - Onnodige eager loading aanwezig is # - Een counter cache gebruikt zou moeten worden ``` De regel: standaard `includes` gebruiken (Rails kiest de optimale strategie), `preload` wanneer aparte queries gewenst zijn, `eager_load` wanneer er op associaties gefilterd wordt. ### Vraag 5: Leg Scopes en Query Objects in Rails uit Scopes kapselen herbruikbare query-voorwaarden in. Voor complexe queries bieden Query Objects een betere organisatie en testbaarheid. ```ruby # app/models/product.rb class Product < ApplicationRecord # Eenvoudige scopes scope :active, -> { where(active: true) } scope :in_stock, -> { where('stock_quantity > 0') } scope :featured, -> { where(featured: true) } # Scopes met parameters scope :cheaper_than, ->(price) { where('price < ?', price) } scope :in_category, ->(category) { where(category: category) } # Ketenbare scopes scope :available, -> { active.in_stock } # Scope met joins scope :with_recent_orders, -> { joins(:order_items) .where('order_items.created_at > ?', 30.days.ago) .distinct } # Scope met 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 voor complexe zoekopdrachten 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 # Gebruik in de controller @products = ProductsSearchQuery.new(Product.active).call(params) ``` Scopes zijn perfect voor eenvoudige, herbruikbare voorwaarden. Query Objects passen bij complexe zoekopdrachten met meerdere optionele filters en compositielogica. ## Routing en Controllers ### Vraag 6: Hoe werkt RESTful routing in Rails? Rails moedigt RESTful routes aan die HTTP-werkwoorden koppelen aan CRUD-acties. De router vertaalt URL's naar specifieke controller-aanroepen. ```ruby # config/routes.rb Rails.application.routes.draw do # Standaard RESTful routes (7 acties) resources :articles do # Geneste routes resources :comments, only: [:create, :destroy] # Member routes (op een instantie) member do post :publish delete :archive end # Collection routes (op de collectie) collection do get :drafts get :search end end # API-routes met namespace namespace :api do namespace :v1 do resources :products, only: [:index, :show, :create, :update] do resources :reviews, shallow: true end end end # Aangepaste route get 'dashboard', to: 'dashboard#index' # Routebeperkingen constraints(SubdomainConstraint.new) do resources :admin_settings end # Hoofdroute root 'home#index' end ``` ```bash # rails routes - Toont alle gegenereerde routes # # 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 ``` Gegenereerde route-helpers (`article_path(@article)`, `new_article_path`) maken het mogelijk om URL's dynamisch en onderhoudbaar te refereren. ### Vraag 7: Leg callbacks en filters in controllers uit Callbacks (before_action, after_action, around_action) maken het mogelijk om code voor, na of rond controlleracties uit te voeren. ```ruby # app/controllers/application_controller.rb class ApplicationController < ActionController::Base # CSRF-bescherming standaard ingeschakeld protect_from_forgery with: :exception # Globale callback voor authenticatie before_action :authenticate_user! # Globale foutafhandeling rescue_from ActiveRecord::RecordNotFound, with: :not_found rescue_from ActionController::ParameterMissing, with: :bad_request private def not_found render json: { error: 'Resource niet gevonden' }, 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 met opties before_action :require_admin before_action :set_product, only: [:show, :edit, :update, :destroy] after_action :log_activity, only: [:create, :update, :destroy] # Voorwaardelijke 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: 'Product aangemaakt.' else render :new, status: :unprocessable_entity end end def update if @product.update(product_params) redirect_to [:admin, @product], notice: 'Product bijgewerkt.' 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 worden uitgevoerd in volgorde van declaratie. Gebruik `skip_before_action` in subklassen om geërfde callbacks uit te schakelen. Vermijd callbacks met te veel bedrijfslogica: geef de voorkeur aan Service Objects. ## Services en architectuur ### Vraag 8: Hoe Service Objects in Rails implementeren? Service Objects kapselen complexe bedrijfslogica in die niet thuishoort in Models of Controllers. Ze verbeteren de testbaarheid en volgen het Single Responsibility-principe. ```ruby # app/services/order_processor.rb # Service Object met gestandaardiseerde interface class OrderProcessor def initialize(order, payment_method:) @order = order @payment_method = payment_method end def call return failure('Bestelling al verwerkt') 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("Betaling mislukt: #{e.message}") rescue InsufficientStockError => e failure("Voorraad onvoldoende: #{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: "Bestelling ##{@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: 'Bestelling bevestigd!' 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 ``` Het Service Object-patroon volgt een eenvoudige conventie: één klasse, één verantwoordelijkheid, één publieke `call`-methode. Een Result-object teruggeven maakt een nette afhandeling van succes en mislukking mogelijk. ### Vraag 9: Leg Concerns in Rails uit Concerns maken het mogelijk om code tussen Models of Controllers te extraheren en te delen. Ze gebruiken `ActiveSupport::Concern` voor een schone include-syntax. ```ruby # app/models/concerns/sluggable.rb # Herbruikbare Concern voor het genereren van slugs module Sluggable extend ActiveSupport::Concern included do # Code die bij include wordt uitgevoerd before_validation :generate_slug, if: :should_generate_slug? validates :slug, presence: true, uniqueness: true end # Klassemethoden 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 # Instantiemethoden 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 # Optioneel, :title als standaard end # app/models/product.rb class Product < ApplicationRecord include Sluggable sluggable_source :name end ``` ```ruby # app/controllers/concerns/pagination.rb # Concern voor controllers 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 zijn nuttig voor echt gedeelde code. Vermijd het maken van Concerns alleen om een Model "in te korten": dat verbergt complexiteit zonder die te verminderen. ## Testen met RSpec ### Vraag 10: Hoe RSpec-tests structureren in Rails? RSpec is het standaard testframework voor Rails. Een goede teststructuur omvat Model specs, Controller specs, Service specs en integratietesten. ```ruby # spec/models/user_spec.rb require 'rails_helper' RSpec.describe User, type: :model do # Factories met 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 'valideert het e-mailformaat' 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 'geeft voor- en achternaam gecombineerd terug' do user = build(:user, first_name: 'John', last_name: 'Doe') expect(user.full_name).to eq('John Doe') end it 'verwerkt ontbrekende achternaam' do user = build(:user, first_name: 'John', last_name: nil) expect(user.full_name).to eq('John') end end describe '.active' do it 'geeft alleen actieve gebruikers terug' 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 'wanneer de bestelling geldig is' do before do allow(PaymentGateway).to receive(:charge).and_return( OpenStruct.new(success?: true, transaction_id: 'txn_123') ) end it 'verwerkt de bestelling met succes' do result = subject.call expect(result).to be_success expect(order.reload.status).to eq('completed') end it 'verlaagt de productvoorraad' do expect { subject.call }.to change { product.reload.stock_quantity }.by(-2) end it 'verstuurt de bevestigingsmail' do expect { subject.call } .to have_enqueued_mail(OrderMailer, :confirmation) .with(order) end end context 'wanneer de betaling mislukt' do before do allow(PaymentGateway).to receive(:charge).and_return( OpenStruct.new(success?: false, error: 'Card declined') ) end it 'geeft een mislukkingsresultaat terug' do result = subject.call expect(result).to be_failure expect(result.error).to include('Card declined') end it 'werkt de bestellingsstatus niet bij' 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 'geeft de productlijst terug' 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 op categorie' 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: 'Nieuw Product', price: 99.99, category_id: create(:category).id } } end it 'maakt een nieuw product aan' 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: gebruik `let` voor data, `describe` voor methoden/contexten, `context` voor voorwaarden en `it` voor specifieke asserties. Eén test moet één ding testen. ### Vraag 11: Hoe factories met FactoryBot gebruiken? FactoryBot maakt het mogelijk om testdata declaratief en onderhoudbaar aan te maken. Factories vervangen statische fixtures. ```ruby # spec/factories/users.rb FactoryBot.define do factory :user do # Sequenties voor uniciteit 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 voor variaties 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 # Geërfde 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 # Gebruik in tests RSpec.describe OrderProcessor do # build: niet-gepersisteerde instantie let(:user) { build(:user) } # create: gepersisteerd in DB let(:order) { create(:order, :with_items, user: user) } # create_list: meerdere instanties let(:products) { create_list(:product, 5) } # Traits combineren let(:admin) { create(:user, :admin, :with_profile) } # Attributen overschrijven let(:expensive_order) { create(:order, :with_items, items_count: 10) } # build_stubbed: sneller, voor unit tests let(:stubbed_user) { build_stubbed(:user) } end ``` Geef de voorkeur aan `build` of `build_stubbed` boven `create` wanneer persistentie niet nodig is: dit versnelt de tests aanzienlijk. ## Background Jobs ### Vraag 12: Hoe Active Job en Sidekiq in Rails gebruiken? Active Job biedt een uniforme interface voor background jobs, ongeacht het backend (Sidekiq, Resque, enz.). Sidekiq is de populaire keuze vanwege zijn performance met Redis. ```ruby # app/jobs/process_order_job.rb class ProcessOrderJob < ApplicationJob queue_as :default # Retry-configuratie retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3 retry_on Net::OpenTimeout, wait: :polynomially_longer, attempts: 10 discard_on ActiveJob::DeserializationError # Sidekiq-opties (bij Sidekiq-backend) sidekiq_options retry: 5, backtrace: true def perform(order_id) order = Order.find(order_id) OrderProcessor.new(order).call rescue ActiveRecord::RecordNotFound # Bestelling verwijderd tussen enqueue en uitvoering 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 met Sidekiq Enterprise of 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 in de wachtrij plaatsen # Onmiddellijk ProcessOrderJob.perform_later(order.id) # Vertraagd ProcessOrderJob.set(wait: 5.minutes).perform_later(order.id) # Op een specifiek tijdstip ProcessOrderJob.set(wait_until: Date.tomorrow.noon).perform_later(order.id) # Specifieke wachtrij ProcessOrderJob.set(queue: :critical).perform_later(order.id) # Synchroon (voor tests of debugging) ProcessOrderJob.perform_now(order.id) ``` ```ruby # config/sidekiq.yml :concurrency: 10 :queues: - [critical, 3] # Hoge prioriteit, gewicht 3 - [default, 2] # Gemiddelde prioriteit, gewicht 2 - [mailers, 1] # Lage prioriteit, gewicht 1 - [low, 1] :schedule: cleanup_job: cron: '0 3 * * *' # Elke dag om 3 uur class: CleanupJob ``` Active Job abstraheert het backend, maar toegang tot specifieke functies (batches, rate limiting) vereist vaak koppeling met het gekozen backend. ## API-ontwikkeling ### Vraag 13: Hoe een RESTful API met Rails bouwen? Rails maakt het eenvoudig om JSON-API's te bouwen met API-only Controllers en serializers. Een goede API is geversioneerd, gedocumenteerd en veilig. ```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: 'Resource niet gevonden', details: exception.message }, status: :not_found end def unprocessable_entity(exception) render json: { error: 'Validatie mislukt', details: exception.record.errors }, status: :unprocessable_entity end def bad_request(exception) render json: { error: 'Ongeldig verzoek', 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 # Met de 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: versioneren via namespace, geschikte HTTP-codes gebruiken, collecties pagineren en duidelijke foutmeldingen geven. ### Vraag 14: Hoe JWT-authenticatie in Rails implementeren? JWT (JSON Web Tokens) is een populaire stateless authenticatiemethode voor API's. De token codeert de identiteit en geldigheid van de gebruiker. ```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 verlopen' rescue JWT::DecodeError raise AuthenticationError, 'Ongeldig 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: 'Ongeldige inloggegevens' }, 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 ontbreekt' 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: 'Gebruiker niet gevonden' }, status: :unauthorized end def current_user @current_user end end ``` Voor productie overwegen: refresh tokens, blacklisting van tokens bij logout en korte verlooptijden. Gems zoals `devise-jwt` vereenvoudigen de implementatie. ## Caching en performance ### Vraag 15: Hoe caching in Rails implementeren? Rails biedt verschillende cachingniveaus: fragment caching, Russian Doll caching, low-level caching. De keuze hangt af van het gebruiksgeval. ```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 met automatische cache-sleutel %> <% @products.each do |product| %> <%# Cache gebaseerd op updated_at van product %> <% cache product do %> <%= render product %> <% end %> <% end %> <%# Russian Doll caching - geneste cache %> <% cache ['v1', @category] do %>

<%= @category.name %>

<% @category.products.each do |product| %> <% cache ['v1', product] do %> <%= render product %> <% end %> <% end %> <% end %> <%# Voorwaardelijke cache %> <% cache_if current_user.nil?, @product do %> <%= render @product %> <% end %> ``` ```ruby # app/models/product.rb class Product < ApplicationRecord # Touch op de parent om Russian Doll-cache te invalideren belongs_to :category, touch: true # Aangepaste cache-sleutel 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 met bescherming tegen race conditions Rails.cache.fetch('popular_products', expires_in: 1.hour, race_condition_ttl: 10.seconds) do Product.bestsellers.limit(10).to_a end # Expliciete invalidatie Rails.cache.delete('dashboard:stats') Rails.cache.delete_matched('products:*') ``` Russian Doll caching is effectief omdat alleen gewijzigde fragmenten opnieuw worden gegenereerd. Gebruik `touch: true` op associaties om invalidatie te verspreiden. ### Vraag 16: Hoe de performance van een Rails-applicatie optimaliseren? Rails-optimalisatie omvat meerdere aspecten: DB-queries, caching, assets en architectuur. Een methodische aanpak met monitoring is essentieel. ```ruby # Database-optimalisatie # config/database.yml production: pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> prepared_statements: true advisory_locks: true # app/models/order.rb class Order < ApplicationRecord # Samengestelde indexen voor frequente queries # add_index :orders, [:user_id, :status, :created_at] # Alleen de benodigde kolommen selecteren scope :summary, -> { select(:id, :status, :total, :created_at) } # Batchverwerking voor grote volumes def self.process_pending pending.find_each(batch_size: 1000) do |order| ProcessOrderJob.perform_later(order.id) end end # Herhalende berekeningen vermijden def self.revenue_by_month completed .group("DATE_TRUNC('month', created_at)") .sum(:total) end end ``` ```ruby # Geheugenoptimalisatie # 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 met 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 en paginering class ProductsController < ApplicationController def index @products = Product.active .includes(:category, :primary_image) .page(params[:page]) .per(24) # Prefetch voor de volgende pagina 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 ``` Essentiële tools: `rack-mini-profiler` voor profiling, `bullet` voor N+1-detectie, `New Relic` of `Scout` voor productiemonitoring. ## Beveiliging ### Vraag 17: Wat zijn de beveiligingsbest practices in Rails? Rails bevat standaardbeveiligingen tegen veelvoorkomende kwetsbaarheden. Het is cruciaal deze beveiligingen te begrijpen en correct te configureren. ```ruby # CSRF-bescherming # app/controllers/application_controller.rb class ApplicationController < ActionController::Base # Standaard ingeschakeld, gooit een exception als het token ongeldig is protect_from_forgery with: :exception # Voor API's :null_session gebruiken # protect_from_forgery with: :null_session end # In views wordt het token automatisch in formulieren opgenomen # <%= form_with ... %> bevat authenticity_token # Voor AJAX-verzoeken # Voeg de header X-CSRF-Token toe met de waarde van csrf_meta_tags ``` ```ruby # SQL Injection-preventie # ✅ Geïnterpoleerde parameters worden automatisch ge-escaped User.where('email = ?', params[:email]) User.where(email: params[:email]) # ❌ GEVAAR - Directe interpolatie User.where("email = '#{params[:email]}'") # ✅ Voor dynamische ORDER-clausules ALLOWED_SORTS = %w[name created_at price].freeze sort_column = ALLOWED_SORTS.include?(params[:sort]) ? params[:sort] : 'name' Product.order(sort_column) ``` ```ruby # XSS-bescherming # Rails escapet HTML in views automatisch # ✅ Automatisch ge-escaped <%= user.name %> # ❌ Gevaarlijk - niet ge-escapete inhoud <%== user.bio %> <%= raw user.bio %> <%= user.bio.html_safe %> # ✅ Voor veilige HTML, sanitize gebruiken <%= 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 # Expliciete whitelist van toegestane attributen params.require(:user).permit(:name, :email, :avatar) # Alleen voor administrators if current_user.admin? params.require(:user).permit(:name, :email, :role, :active) else params.require(:user).permit(:name, :email) end end end ``` ```ruby # Beveiligingsheaders # 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 ``` Regelmatig auditen met `brakeman` (statische beveiligingsanalyse) en gems up-to-date houden met `bundle audit`. ### Vraag 18: Hoe authenticatie en autorisatie in Rails afhandelen? Authenticatie verifieert de identiteit, autorisatie regelt de rechten. Devise beheert auth, Pundit of CanCanCan beheren autorisatie. ```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 voor collecties 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 met 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 bijgewerkt.' 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 gepubliceerd.' end rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized private def user_not_authorized flash[:alert] = "U bent niet bevoegd om deze actie uit te voeren." redirect_back(fallback_location: root_path) end end ``` Pundit is explicieter en beter testbaar dan CanCanCan. Elke actie heeft een corresponderende policy-methode en scopes filteren collecties automatisch. ## Geavanceerd Rails ### Vraag 19: Leg het Repository-patroon in Rails uit Het Repository-patroon isoleert de data access-logica van de rest van de applicatie. Hoewel Rails Active Record gebruikt (een ander patroon), kan Repository nuttig zijn voor complexe gevallen. ```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 # Gebruik in een 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 # Maakt testen met mocks gemakkelijk RSpec.describe ProductSearchService do let(:repository) { instance_double(ProductRepository) } let(:service) { described_class.new(repository: repository) } it 'filtert op categorie' 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 is optioneel in Rails omdat Active Record al een uitstekend patroon is. Gebruik het voor complexe queries of wanneer storage-isolatie belangrijk is. ### Vraag 20: Hoe het CQRS-patroon in Rails implementeren? CQRS (Command Query Responsibility Segregation) scheidt lees- en schrijfoperaties. In Rails vertaalt dit zich naar afzonderlijke klassen voor queries en 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, "Product #{item[:product_id]} niet beschikbaar") 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 die CQRS gebruikt 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: 'Bestelling aangemaakt!' else flash.now[:alert] = result.errors.join(', ') render :new, status: :unprocessable_entity end end end ``` CQRS schittert in complexe applicaties met asymmetrische lees-/schrijfbehoeften. Voor eenvoudig CRUD is het over-engineering. ### Vraag 21: Hoe WebSockets met Action Cable afhandelen? Action Cable integreert WebSockets in Rails voor bidirectionele realtime communicatie. Het gebruikt Redis voor synchronisatie tussen servers. ```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 # Via sessie-cookie if verified_user = User.find_by(id: cookies.encrypted[:user_id]) verified_user # Via JWT voor API's 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]) # Rechten controleren unless @room.accessible_by?(current_user) reject return end stream_for @room # Anderen op de hoogte stellen van aanwezigheid 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'] ) # Naar alle abonnees broadcasten 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 handelt heraansluitingen en synchronisatie automatisch af. In productie Redis als adapter configureren en schalen op basis van gelijktijdige verbindingen. ### Vraag 22: Hoe multi-tenancy in Rails implementeren? Multi-tenancy stelt een applicatie in staat om meerdere geïsoleerde klanten (tenants) te bedienen. Drie hoofdaanpakken: op database-, schema- of rij-niveau. ```ruby # Multitenancy op rij-niveau met ActsAsTenant of handmatig # app/models/concerns/tenant_scoped.rb module TenantScoped extend ActiveSupport::Concern included do belongs_to :tenant # Standaardscope op de huidige tenant default_scope -> { where(tenant: Current.tenant) if Current.tenant } # Tenant-validatie 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 # Via subdomein if request.subdomain.present? && request.subdomain != 'www' Tenant.find_by!(subdomain: request.subdomain) # Via header (voor API's) elsif request.headers['X-Tenant-ID'].present? Tenant.find(request.headers['X-Tenant-ID']) # Via gebruiker elsif current_user current_user.tenant end rescue ActiveRecord::RecordNotFound redirect_to root_url(subdomain: 'www'), alert: 'Tenant niet gevonden' 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 # Administrators kunnen tot meerdere tenants behoren has_many :tenant_memberships has_many :accessible_tenants, through: :tenant_memberships, source: :tenant end ``` ```ruby # Op schema-niveau met de Apartment-gem (PostgreSQL) # config/initializers/apartment.rb Apartment.configure do |config| config.excluded_models = %w[Tenant User] config.tenant_names = -> { Tenant.pluck(:subdomain) } end # Gebruik Apartment::Tenant.switch('acme') do # Alle queries in dit blok gebruiken het 'acme'-schema Project.all # SELECT * FROM acme.projects end ``` Rij-niveau is het eenvoudigst maar vereist constante aandacht voor lekken. Schema-niveau biedt betere isolatie maar bemoeilijkt migraties. Kies op basis van beveiligings- en schaalbaarheidseisen. ### Vraag 23: Hoe een microservices-architectuur met Rails opzetten? Rails kan dienen als basis voor een microservices-architectuur met communicatie via HTTP/gRPC of message queues. De sleutel is het goed definiëren van de 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 niet beschikbaar', code: response.code) end rescue Net::OpenTimeout, Net::ReadTimeout ServiceResult.failure('Service-timeout') end end ``` ```ruby # Event-driven communicatie met 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-patroon # app/controllers/api/v1/gateway_controller.rb module Api module V1 class GatewayController < BaseController # Meerdere services aggregeren 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} niet beschikbaar", message: e.message } end end end end ``` Voor Rails-microservices: heldere API-contracten definiëren (OpenAPI), circuit breakers implementeren (gem `circuitbox`) en distributed tracing gebruiken (gem `opentelemetry`). ### Vraag 24: Hoe een Rails-applicatie naar productie deployen? Moderne Rails-deployment maakt gebruik van containers of PaaS. Een robuuste productieconfiguratie omvat assets, database en monitoring. ```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 forceren 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 # Productie-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: ``` Productie-checklist: SSL verplicht, secrets via ENV, health checks, geautomatiseerde DB-backups, monitoring (APM + logs + metrics) en geconfigureerde alerting. ### Vraag 25: Wat zijn de nieuwe features in Rails 7+ die je moet kennen? Rails 7+ brengt belangrijke wijzigingen: Hotwire standaard, import maps, verbeterde versleutelde credentials en talloze optimalisaties. ```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 "Meer laden", articles_path(page: @page + 1), data: { turbo_frame: "articles" } %> <% end %> # Turbo Streams voor realtime 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-controllers # 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 (zonder 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 vanaf 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 # Zoekopdrachten mogelijk encrypts :phone_number # Niet-deterministisch standaard 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 # Verbeteringen in de query-interface # Rails 7.1+ # Asynchrone queries users = User.where(active: true).load_async # Verder verwerken terwijl de query loopt # Resultaten openen met users.to_a # 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-detectie class Author < ApplicationRecord has_many :books # inverse_of automatisch gedetecteerd end # Strict loading standaard (vermijdt N+1) class ApplicationRecord < ActiveRecord::Base self.strict_loading_by_default = true end ``` Rails 7+ geeft de voorkeur aan eenvoud (geen Webpack standaard) en HTML-over-the-wire met Hotwire. Deze aanpak vermindert de JavaScript-complexiteit en biedt tegelijkertijd een moderne gebruikerservaring. ## Conclusie Ruby on Rails-sollicitatiegesprekken beoordelen de beheersing van het hele framework en het begrip van de conventies. Belangrijke punten om te onthouden: ✅ **Grondbeginselen**: MVC, Active Record, migraties, validaties en associaties ✅ **Architectuur**: Service Objects, Concerns, Query Objects en CQRS-patronen ✅ **Performance**: N+1-queries, caching (fragment, Russian Doll, low-level), eager loading ✅ **Testen**: RSpec, FactoryBot, request specs en testing best practices ✅ **Beveiliging**: CSRF, SQL injection, XSS, Strong Parameters en authenticatie/autorisatie ✅ **API's**: RESTful design, JWT, serializers en versionering ✅ **Productie**: background jobs, WebSockets, deployment en monitoring De Rails-filosofie (Convention over Configuration, DRY en Rails Way) stuurt alle architecturale beslissingen. Deze principes beheersen en weten wanneer ervan af te wijken, toont solide expertise. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/nl/blog/ruby-on-rails/ruby-on-rails-interview-questions