# Pytania na rozmowę Ruby on Rails: Top 25 w 2026 > 25 najczęściej zadawanych pytań na rozmowach Ruby on Rails. Architektura MVC, Active Record, migracje, testowanie RSpec, REST API ze szczegółowymi odpowiedziami i przykładami kodu. - Published: 2026-02-03 - Updated: 2026-04-27 - Author: Anthony Fillion-Maillet - Tags: ruby on rails, ruby, rozmowa kwalifikacyjna, active record, rozmowa techniczna - Reading time: 22 min --- Rozmowy kwalifikacyjne z Ruby on Rails sprawdzają znajomość najpopularniejszego frameworka Ruby, zrozumienie architektury MVC, ORM-a Active Record oraz umiejętność budowania solidnych aplikacji webowych zgodnie z filozofią "Convention over Configuration". Niniejszy przewodnik obejmuje 25 najczęściej zadawanych pytań, od podstaw Rails po zaawansowane wzorce produkcyjne. > **Wskazówka rekrutacyjna** > > Rekruterzy doceniają kandydatów rozumiejących filozofię Rails: "Convention over Configuration", DRY (Don't Repeat Yourself) i wzorce Rails Way. Wyjaśnienie, dlaczego Rails podejmuje określone decyzje architektoniczne, robi różnicę. ## Podstawy Ruby on Rails ### Pytanie 1: Wyjaśnij wzorzec MVC w Ruby on Rails Wzorzec Model-View-Controller (MVC) stanowi rdzeń architektoniczny Rails. Rozdziela odpowiedzialności na trzy odrębne warstwy, co poprawia łatwość utrzymania i testowalność kodu. ```ruby # app/models/article.rb # Model zarządza danymi i logiką biznesową class Article < ApplicationRecord # Walidacje danych validates :title, presence: true, length: { minimum: 5 } validates :body, presence: true # Powiązania z innymi modelami belongs_to :author, class_name: 'User' has_many :comments, dependent: :destroy has_many :tags, through: :article_tags # Scope'y dla zapytań wielokrotnego użytku scope :published, -> { where(published: true) } scope :recent, -> { order(created_at: :desc).limit(10) } # Callbacki cyklu życia before_save :generate_slug private def generate_slug self.slug = title.parameterize if title_changed? end end ``` ```ruby # app/controllers/articles_controller.rb # Controller odbiera żądania i orkiestruje odpowiedź 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: 'Artykuł został utworzony.' 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 %> <%# View wyświetla dane w formacie HTML %>

<%= @article.title %>

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

<%= simple_format @article.body %>
<%# Partial dla komentarzy %> <%= render @comments %>
``` Typowy przepływ: żądanie trafia do Routera, który kieruje je do odpowiedniego Controllera. Controller współpracuje z Modelem w celu pobrania lub modyfikacji danych, a następnie przekazuje te dane do View w celu wyrenderowania HTML. ### Pytanie 2: Czym jest Active Record i jak działa ORM Rails? Active Record to ORM (Object-Relational Mapping) Rails implementujący wzorzec Active Record. Każda klasa Modelu reprezentuje tabelę bazy danych, a każda instancja reprezentuje pojedynczy wiersz. ```ruby # app/models/user.rb # Active Record automatycznie mapuje kolumny na atrybuty class User < ApplicationRecord # Tabela 'users' jest powiązana automatycznie # Kolumny: id, email, name, created_at, updated_at has_secure_password # BCrypt dla hasła has_many :articles, foreign_key: :author_id has_one :profile, dependent: :destroy has_and_belongs_to_many :roles # Walidacje validates :email, presence: true, uniqueness: { case_sensitive: false }, format: { with: URI::MailTo::EMAIL_REGEXP } # Callbacki before_save :normalize_email # Metody klasowe dla zapytań def self.admins joins(:roles).where(roles: { name: 'admin' }) end private def normalize_email self.email = email.downcase.strip end end ``` ```ruby # Przykłady zapytań Active Record # Konsola Rails lub wnętrze serwisu # Tworzenie user = User.create!(email: 'dev@example.com', name: 'Alice', password: 'secret123') # Odczyt z warunkami active_users = User.where(active: true).order(:name) user = User.find_by(email: 'dev@example.com') # Zapytania łańcuchowe (lazy evaluation) recent_admins = User.admins .where('created_at > ?', 1.month.ago) .includes(:profile) .limit(10) # Zapobieganie N+1 dzięki eager loading articles = Article.includes(:author, :comments).published # Aktualizacja user.update!(name: 'Alice Martin') # Transakcje User.transaction do user.debit_balance!(100) recipient.credit_balance!(100) Payment.create!(from: user, to: recipient, amount: 100) end ``` Active Record konwertuje metody Ruby na zoptymalizowane zapytania SQL. Metody takie jak `where`, `joins`, `includes` są leniwe: zapytanie wykonuje się dopiero przy iteracji lub wywołaniu `to_a`. ### Pytanie 3: Wyjaśnij system migracji Rails Migracje pozwalają wersjonować schemat bazy danych za pomocą Ruby. Są odwracalne i umożliwiają kontrolowaną ewolucję struktury danych. ```ruby # db/migrate/20260203100000_create_products.rb # Migracja tworząca tabelę 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 i updated_at automatycznie end # Indeksy dla wydajności add_index :products, :name add_index :products, [:category_id, :active] end end ``` ```ruby # db/migrate/20260203110000_add_slug_to_products.rb # Migracja modyfikująca istniejącą tabelę class AddSlugToProducts < ActiveRecord::Migration[7.1] def change add_column :products, :slug, :string add_index :products, :slug, unique: true # Wypełnienie istniejących slugów reversible do |dir| dir.up do Product.find_each do |product| product.update_column(:slug, product.name.parameterize) end end end # Ustawienie NOT NULL po wypełnieniu change_column_null :products, :slug, false end end ``` ```bash # Kluczowe komendy migracji rails db:migrate # Wykonuje oczekujące migracje rails db:rollback # Cofa ostatnią migrację rails db:rollback STEP=3 # Cofa ostatnie 3 migracje rails db:migrate:status # Pokazuje status migracji rails db:seed # Uruchamia db/seeds.rb rails db:reset # Drop, create, migrate, seed ``` Migracje muszą być odwracalne. Metoda `change` jest inteligentna i potrafi automatycznie odwracać typowe operacje. Dla złożonych przypadków używać `up` i `down` osobno. ## Zaawansowane Active Record ### Pytanie 4: Jak optymalizować zapytania N+1 w Rails? Problem N+1 występuje, gdy po początkowym zapytaniu następuje N dodatkowych zapytań do załadowania powiązań. Rails udostępnia kilka metod eager loading, aby rozwiązać ten problem. ```ruby # app/controllers/orders_controller.rb class OrdersController < ApplicationController def index # ❌ PROBLEM N+1: 1 zapytanie + N zapytań na zamówienie # @orders = Order.all # W widoku: order.user.name generuje zapytanie na zamówienie # ✅ ROZWIĄZANIE z includes (eager loading) @orders = Order.includes(:user, :items) .where(status: 'completed') .order(created_at: :desc) # Generuje łącznie tylko 3 zapytania end def show # includes: ładuje powiązania osobno (2-3 zapytania) @order = Order.includes(items: :product).find(params[:id]) # preload: wymusza osobne ładowanie @order = Order.preload(:items, :user).find(params[:id]) # eager_load: wymusza LEFT OUTER JOIN (1 zapytanie) @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 z domyślnym includes scope :with_details, -> { includes(:user, items: :product) } # Counter cache, aby uniknąć zapytań COUNT # Wymaga: add_column :users, :orders_count, :integer, default: 0 belongs_to :user, counter_cache: true end ``` ```ruby # Wykrywanie N+1 z gemem Bullet (środowisko developerskie) # config/environments/development.rb config.after_initialize do Bullet.enable = true Bullet.alert = true Bullet.bullet_logger = true Bullet.rails_logger = true end # Bullet pokaże alerty, gdy: # - Zostanie wykryte zapytanie N+1 # - Wystąpi niepotrzebne eager loading # - Powinien zostać użyty counter cache ``` Zasada: domyślnie używać `includes` (Rails wybiera optymalną strategię), `preload` gdy chcemy wymusić osobne zapytania, `eager_load` gdy filtrujemy po powiązaniach. ### Pytanie 5: Wyjaśnij Scope'y i Query Objects w Rails Scope'y enkapsulują wielokrotnego użytku warunki zapytań. Dla skomplikowanych zapytań Query Objects oferują lepszą organizację i testowalność. ```ruby # app/models/product.rb class Product < ApplicationRecord # Proste scope'y scope :active, -> { where(active: true) } scope :in_stock, -> { where('stock_quantity > 0') } scope :featured, -> { where(featured: true) } # Scope'y z parametrami scope :cheaper_than, ->(price) { where('price < ?', price) } scope :in_category, ->(category) { where(category: category) } # Łączone scope'y scope :available, -> { active.in_stock } # Scope z joins scope :with_recent_orders, -> { joins(:order_items) .where('order_items.created_at > ?', 30.days.ago) .distinct } # Scope z podzapytaniem 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 dla złożonych wyszukiwań 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 # Użycie w kontrolerze @products = ProductsSearchQuery.new(Product.active).call(params) ``` Scope'y są idealne dla prostych, wielokrotnego użytku warunków. Query Objects sprawdzają się przy złożonych wyszukiwaniach z wieloma opcjonalnymi filtrami i logiką kompozycji. ## Routing i kontrolery ### Pytanie 6: Jak działa routing RESTful w Rails? Rails zachęca do tras RESTful, które mapują czasowniki HTTP na akcje CRUD. Router tłumaczy adresy URL na konkretne wywołania kontrolerów. ```ruby # config/routes.rb Rails.application.routes.draw do # Standardowe trasy RESTful (7 akcji) resources :articles do # Trasy zagnieżdżone resources :comments, only: [:create, :destroy] # Trasy member (działają na instancji) member do post :publish delete :archive end # Trasy collection (działają na kolekcji) collection do get :drafts get :search end end # Trasy API z namespace namespace :api do namespace :v1 do resources :products, only: [:index, :show, :create, :update] do resources :reviews, shallow: true end end end # Trasa niestandardowa get 'dashboard', to: 'dashboard#index' # Ograniczenia tras constraints(SubdomainConstraint.new) do resources :admin_settings end # Trasa główna root 'home#index' end ``` ```bash # rails routes - Pokazuje wszystkie wygenerowane trasy # # 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 ``` Wygenerowane helpery tras (`article_path(@article)`, `new_article_path`) pozwalają odwoływać się do URL-i w sposób dynamiczny i łatwy w utrzymaniu. ### Pytanie 7: Wyjaśnij callbacki i filtry w kontrolerach Callbacki (before_action, after_action, around_action) pozwalają wykonywać kod przed, po lub wokół akcji kontrolera. ```ruby # app/controllers/application_controller.rb class ApplicationController < ActionController::Base # Ochrona CSRF włączona domyślnie protect_from_forgery with: :exception # Globalny callback uwierzytelniania before_action :authenticate_user! # Globalna obsługa błędów rescue_from ActiveRecord::RecordNotFound, with: :not_found rescue_from ActionController::ParameterMissing, with: :bad_request private def not_found render json: { error: 'Zasób nie został znaleziony' }, 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 # Callbacki z opcjami before_action :require_admin before_action :set_product, only: [:show, :edit, :update, :destroy] after_action :log_activity, only: [:create, :update, :destroy] # Callback warunkowy 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 utworzony.' else render :new, status: :unprocessable_entity end end def update if @product.update(product_params) redirect_to [:admin, @product], notice: 'Produkt zaktualizowany.' 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 ``` Callbacki wykonują się w kolejności deklaracji. Używać `skip_before_action` w podklasach, aby wyłączyć dziedziczone callbacki. Unikać callbacków z nadmiarem logiki biznesowej: preferować Service Objects. ## Serwisy i architektura ### Pytanie 8: Jak implementować Service Objects w Rails? Service Objects enkapsulują skomplikowaną logikę biznesową, która nie należy ani do Modeli, ani do Kontrolerów. Poprawiają testowalność i przestrzegają zasady pojedynczej odpowiedzialności. ```ruby # app/services/order_processor.rb # Service Object ze znormalizowanym interfejsem class OrderProcessor def initialize(order, payment_method:) @order = order @payment_method = payment_method end def call return failure('Zamówienie już przetworzone') 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("Płatność nie powiodła się: #{e.message}") rescue InsufficientStockError => e failure("Niewystarczający stan magazynowy: #{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: "Zamówienie ##{@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: 'Zamówienie potwierdzone!' 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 ``` Wzorzec Service Object kieruje się prostą konwencją: jedna klasa, jedna odpowiedzialność, jedna publiczna metoda `call`. Zwracanie obiektu Result umożliwia czytelne obsługiwanie sukcesu i porażki. ### Pytanie 9: Wyjaśnij Concerns w Rails Concerns pozwalają wyodrębnić i współdzielić kod między Modelami lub Kontrolerami. Wykorzystują `ActiveSupport::Concern`, aby zapewnić czystą składnię włączania. ```ruby # app/models/concerns/sluggable.rb # Concern wielokrotnego użytku do generowania slugów module Sluggable extend ActiveSupport::Concern included do # Kod wykonywany przy włączeniu before_validation :generate_slug, if: :should_generate_slug? validates :slug, presence: true, uniqueness: true end # Metody klasowe 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 # Metody instancji 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 # Opcjonalne, :title domyślnie end # app/models/product.rb class Product < ApplicationRecord include Sluggable sluggable_source :name end ``` ```ruby # app/controllers/concerns/pagination.rb # Concern dla kontrolerów 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 są przydatne dla rzeczywiście współdzielonego kodu. Unikać tworzenia Concernów tylko po to, aby "skrócić" Model: ukrywa to złożoność, nie zmniejszając jej. ## Testowanie z RSpec ### Pytanie 10: Jak strukturyzować testy RSpec w Rails? RSpec to standardowy framework testowy dla Rails. Dobra struktura testów obejmuje Model specs, Controller specs, Service specs i testy integracyjne. ```ruby # spec/models/user_spec.rb require 'rails_helper' RSpec.describe User, type: :model do # Fabryki z 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 'sprawdza poprawność formatu email' 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 'zwraca połączone imię i nazwisko' do user = build(:user, first_name: 'John', last_name: 'Doe') expect(user.full_name).to eq('John Doe') end it 'obsługuje brak nazwiska' do user = build(:user, first_name: 'John', last_name: nil) expect(user.full_name).to eq('John') end end describe '.active' do it 'zwraca tylko aktywnych użytkowników' 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 'gdy zamówienie jest poprawne' do before do allow(PaymentGateway).to receive(:charge).and_return( OpenStruct.new(success?: true, transaction_id: 'txn_123') ) end it 'przetwarza zamówienie pomyślnie' do result = subject.call expect(result).to be_success expect(order.reload.status).to eq('completed') end it 'zmniejsza stan magazynowy produktu' do expect { subject.call }.to change { product.reload.stock_quantity }.by(-2) end it 'wysyła e-mail potwierdzający' do expect { subject.call } .to have_enqueued_mail(OrderMailer, :confirmation) .with(order) end end context 'gdy płatność nie powiedzie się' do before do allow(PaymentGateway).to receive(:charge).and_return( OpenStruct.new(success?: false, error: 'Card declined') ) end it 'zwraca rezultat porażki' do result = subject.call expect(result).to be_failure expect(result.error).to include('Card declined') end it 'nie aktualizuje statusu zamówienia' 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 'zwraca listę produktów' do get '/api/v1/products', headers: headers expect(response).to have_http_status(:ok) expect(json_response['data'].size).to eq(3) end it 'filtruje po kategorii' 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: 'Nowy Produkt', price: 99.99, category_id: create(:category).id } } end it 'tworzy nowy 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 ``` Dobre praktyki: używać `let` dla danych, `describe` dla metod/kontekstów, `context` dla warunków oraz `it` dla konkretnych asercji. Jeden test powinien testować jedną rzecz. ### Pytanie 11: Jak używać fabryk z FactoryBot? FactoryBot pozwala tworzyć dane testowe deklaratywnie i z myślą o łatwym utrzymaniu. Fabryki zastępują statyczne fixtures. ```ruby # spec/factories/users.rb FactoryBot.define do factory :user do # Sekwencje gwarantujące unikalność 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 } # Traity dla wariantów 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 # Fabryka dziedziczona 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 # Użycie w testach RSpec.describe OrderProcessor do # build: instancja niezapisana w bazie let(:user) { build(:user) } # create: zapisana w BD let(:order) { create(:order, :with_items, user: user) } # create_list: wiele instancji let(:products) { create_list(:product, 5) } # Łączenie traitów let(:admin) { create(:user, :admin, :with_profile) } # Nadpisanie atrybutów let(:expensive_order) { create(:order, :with_items, items_count: 10) } # build_stubbed: szybciej, dla testów jednostkowych let(:stubbed_user) { build_stubbed(:user) } end ``` Preferować `build` lub `build_stubbed` zamiast `create`, gdy persistencja nie jest potrzebna: znacząco przyspiesza to testy. ## Zadania w tle ### Pytanie 12: Jak używać Active Job i Sidekiq w Rails? Active Job zapewnia ujednolicony interfejs do zadań w tle, niezależnie od backendu (Sidekiq, Resque itp.). Sidekiq to popularny wybór ze względu na wydajność z Redisem. ```ruby # app/jobs/process_order_job.rb class ProcessOrderJob < ApplicationJob queue_as :default # Konfiguracja ponawiania retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3 retry_on Net::OpenTimeout, wait: :polynomially_longer, attempts: 10 discard_on ActiveJob::DeserializationError # Opcje Sidekiq (gdy backend Sidekiq) sidekiq_options retry: 5, backtrace: true def perform(order_id) order = Order.find(order_id) OrderProcessor.new(order).call rescue ActiveRecord::RecordNotFound # Zamówienie usunięte między kolejkowaniem a wykonaniem 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 # Ograniczanie częstotliwości z Sidekiq Enterprise lub gemem throttle 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 # Kolejkowanie zadań # Natychmiastowe ProcessOrderJob.perform_later(order.id) # Opóźnione ProcessOrderJob.set(wait: 5.minutes).perform_later(order.id) # O konkretnej godzinie ProcessOrderJob.set(wait_until: Date.tomorrow.noon).perform_later(order.id) # Konkretna kolejka ProcessOrderJob.set(queue: :critical).perform_later(order.id) # Synchronicznie (do testów lub debugowania) ProcessOrderJob.perform_now(order.id) ``` ```ruby # config/sidekiq.yml :concurrency: 10 :queues: - [critical, 3] # Wysoki priorytet, waga 3 - [default, 2] # Średni priorytet, waga 2 - [mailers, 1] # Niski priorytet, waga 1 - [low, 1] :schedule: cleanup_job: cron: '0 3 * * *' # Codziennie o 3:00 class: CleanupJob ``` Active Job abstrahuje backend, ale dostęp do specyficznych funkcji (batches, rate limiting) zazwyczaj wymaga sprzężenia z wybranym backendem. ## Tworzenie API ### Pytanie 13: Jak zbudować RESTful API z Rails? Rails ułatwia tworzenie API JSON dzięki kontrolerom API-only oraz serializatorom. Dobre API jest wersjonowane, udokumentowane i bezpieczne. ```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: 'Zasób nie znaleziony', details: exception.message }, status: :not_found end def unprocessable_entity(exception) render json: { error: 'Walidacja nie powiodła się', details: exception.record.errors }, status: :unprocessable_entity end def bad_request(exception) render json: { error: 'Nieprawidłowe żądanie', 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 # Z gemem jsonapi-serializer 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 ``` Dobre praktyki API: wersjonować przez namespace, używać odpowiednich kodów HTTP, paginować kolekcje i dostarczać czytelne komunikaty błędów. ### Pytanie 14: Jak implementować uwierzytelnianie JWT w Rails? JWT (JSON Web Tokens) to popularna metoda uwierzytelniania bezstanowego dla API. Token koduje tożsamość i ważność użytkownika. ```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 wygasł' rescue JWT::DecodeError raise AuthenticationError, 'Nieprawidłowy 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: 'Nieprawidłowe dane logowania' }, 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, 'Brak tokena' 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: 'Użytkownik nie znaleziony' }, status: :unauthorized end def current_user @current_user end end ``` W produkcji rozważyć: refresh tokens, blacklisting tokenów po wylogowaniu i krótkie czasy wygaśnięcia. Gemy takie jak `devise-jwt` upraszczają implementację. ## Cache i wydajność ### Pytanie 15: Jak implementować cache w Rails? Rails oferuje kilka poziomów cache: fragment caching, Russian Doll caching, low-level caching. Wybór zależy od przypadku użycia. ```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 z automatycznym kluczem cache %> <% @products.each do |product| %> <%# Cache oparty na updated_at produktu %> <% cache product do %> <%= render product %> <% end %> <% end %> <%# Russian Doll caching - zagnieżdżony cache %> <% cache ['v1', @category] do %>

<%= @category.name %>

<% @category.products.each do |product| %> <% cache ['v1', product] do %> <%= render product %> <% end %> <% end %> <% end %> <%# Cache warunkowy %> <% cache_if current_user.nil?, @product do %> <%= render @product %> <% end %> ``` ```ruby # app/models/product.rb class Product < ApplicationRecord # Touch rodzica, aby unieważnić Russian Doll cache belongs_to :category, touch: true # Niestandardowy klucz cache def cache_key_with_version "#{super}/#{reviews.maximum(:updated_at)&.to_i}" end end ``` ```ruby # Low-level caching w serwisach 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 z ochroną przed wyścigami Rails.cache.fetch('popular_products', expires_in: 1.hour, race_condition_ttl: 10.seconds) do Product.bestsellers.limit(10).to_a end # Jawne unieważnianie Rails.cache.delete('dashboard:stats') Rails.cache.delete_matched('products:*') ``` Russian Doll caching jest skuteczny, bo regenerowane są tylko zmodyfikowane fragmenty. Używać `touch: true` na powiązaniach, aby propagować unieważnienie. ### Pytanie 16: Jak optymalizować wydajność aplikacji Rails? Optymalizacja Rails obejmuje wiele aspektów: zapytania DB, cache, assety i architekturę. Niezbędne jest metodyczne podejście wraz z monitoringiem. ```ruby # Optymalizacja bazy danych # config/database.yml production: pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> prepared_statements: true advisory_locks: true # app/models/order.rb class Order < ApplicationRecord # Indeksy złożone dla częstych zapytań # add_index :orders, [:user_id, :status, :created_at] # Wybieranie tylko potrzebnych kolumn scope :summary, -> { select(:id, :status, :total, :created_at) } # Przetwarzanie wsadowe dla dużych wolumenów def self.process_pending pending.find_each(batch_size: 1000) do |order| ProcessOrderJob.perform_later(order.id) end end # Unikanie powtarzających się obliczeń def self.revenue_by_month completed .group("DATE_TRUNC('month', created_at)") .sum(:total) end end ``` ```ruby # Optymalizacja pamięci # 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 # Profilowanie z 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 i paginacja class ProductsController < ApplicationController def index @products = Product.active .includes(:category, :primary_image) .page(params[:page]) .per(24) # Prefetch dla następnej strony 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 ``` Niezbędne narzędzia: `rack-mini-profiler` do profilowania, `bullet` do wykrywania N+1, `New Relic` lub `Scout` do monitoringu produkcyjnego. ## Bezpieczeństwo ### Pytanie 17: Jakie są najlepsze praktyki bezpieczeństwa w Rails? Rails zawiera domyślne zabezpieczenia przed typowymi podatnościami. Zrozumienie i właściwe skonfigurowanie tych zabezpieczeń jest kluczowe. ```ruby # Ochrona CSRF # app/controllers/application_controller.rb class ApplicationController < ActionController::Base # Włączona domyślnie, rzuca wyjątek przy nieprawidłowym tokenie protect_from_forgery with: :exception # Dla API używać :null_session # protect_from_forgery with: :null_session end # W widokach token jest automatycznie dołączany do formularzy # <%= form_with ... %> zawiera authenticity_token # Dla żądań AJAX # Dodać nagłówek X-CSRF-Token z wartością z csrf_meta_tags ``` ```ruby # Zapobieganie SQL Injection # ✅ Parametry interpolowane są automatycznie escape'owane User.where('email = ?', params[:email]) User.where(email: params[:email]) # ❌ NIEBEZPIECZEŃSTWO - bezpośrednia interpolacja User.where("email = '#{params[:email]}'") # ✅ Dla dynamicznych klauzul ORDER ALLOWED_SORTS = %w[name created_at price].freeze sort_column = ALLOWED_SORTS.include?(params[:sort]) ? params[:sort] : 'name' Product.order(sort_column) ``` ```ruby # Ochrona XSS # Rails automatycznie escape'uje HTML w widokach # ✅ Automatyczne escape'owanie <%= user.name %> # ❌ Niebezpieczne - treść bez escape <%== user.bio %> <%= raw user.bio %> <%= user.bio.html_safe %> # ✅ Dla bezpiecznego HTML użyj sanitize <%= 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 # Jawna whitelist dozwolonych atrybutów params.require(:user).permit(:name, :email, :avatar) # Tylko dla administratorów if current_user.admin? params.require(:user).permit(:name, :email, :role, :active) else params.require(:user).permit(:name, :email) end end end ``` ```ruby # Nagłówki bezpieczeństwa # 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 ``` Regularnie audytować z `brakeman` (statyczna analiza bezpieczeństwa) i utrzymywać aktualne gemy z `bundle audit`. ### Pytanie 18: Jak obsługiwać uwierzytelnianie i autoryzację w Rails? Uwierzytelnianie weryfikuje tożsamość, autoryzacja kontroluje uprawnienia. Devise zarządza autoryzacją wstępną, Pundit lub CanCanCan zarządzają autoryzacją. ```ruby # Konfiguracja Devise # 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 # Polityki Pundit # 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 dla kolekcji 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 # Kontroler z 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: 'Artykuł zaktualizowany.' 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: 'Artykuł opublikowany.' end rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized private def user_not_authorized flash[:alert] = "Brak uprawnień do wykonania tej akcji." redirect_back(fallback_location: root_path) end end ``` Pundit jest bardziej jawny i łatwiejszy do testowania niż CanCanCan. Każda akcja ma odpowiednią metodę polityki, a scope'y automatycznie filtrują kolekcje. ## Zaawansowany Rails ### Pytanie 19: Wyjaśnij wzorzec Repository w Rails Wzorzec Repository izoluje logikę dostępu do danych od reszty aplikacji. Choć Rails używa Active Record (innego wzorca), Repository może być przydatny w złożonych przypadkach. ```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 # Użycie w serwisie 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 # Ułatwia testowanie z mockami RSpec.describe ProductSearchService do let(:repository) { instance_double(ProductRepository) } let(:service) { described_class.new(repository: repository) } it 'filtruje po kategorii' 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 jest opcjonalny w Rails, ponieważ Active Record to już doskonały wzorzec. Stosować go dla złożonych zapytań lub gdy izolacja warstwy storage jest istotna. ### Pytanie 20: Jak implementować wzorzec CQRS w Rails? CQRS (Command Query Responsibility Segregation) rozdziela operacje odczytu i zapisu. W Rails przekłada się to na osobne klasy dla query i command. ```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]} niedostępny") 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 # Kontroler z 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: 'Zamówienie utworzone!' else flash.now[:alert] = result.errors.join(', ') render :new, status: :unprocessable_entity end end end ``` CQRS sprawdza się w złożonych aplikacjach z asymetrycznymi potrzebami odczytu/zapisu. Dla prostego CRUD to nadinżynieria. ### Pytanie 21: Jak obsługiwać WebSockety z Action Cable? Action Cable integruje WebSockety w Rails dla dwukierunkowej komunikacji w czasie rzeczywistym. Wykorzystuje Redis do synchronizacji między serwerami. ```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 # Przez cookie sesji if verified_user = User.find_by(id: cookies.encrypted[:user_id]) verified_user # Przez JWT dla API 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]) # Sprawdzenie uprawnień unless @room.accessible_by?(current_user) reject return end stream_for @room # Powiadomienie innych o obecności 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'] ) # Rozgłoszenie do wszystkich subskrybentów 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 automatycznie obsługuje rekonekcje i synchronizację. W produkcji konfigurować Redis jako adapter i skalować zgodnie z liczbą jednoczesnych połączeń. ### Pytanie 22: Jak implementować multi-tenancy w Rails? Multi-tenancy pozwala aplikacji obsługiwać wielu odizolowanych klientów (tenantów). Trzy główne podejścia: na poziomie bazy, schematu lub wiersza. ```ruby # Multitenancy na poziomie wiersza z ActsAsTenant lub ręcznie # app/models/concerns/tenant_scoped.rb module TenantScoped extend ActiveSupport::Concern included do belongs_to :tenant # Domyślny scope na bieżącego tenanta default_scope -> { where(tenant: Current.tenant) if Current.tenant } # Walidacja tenanta 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 # Przez subdomenę if request.subdomain.present? && request.subdomain != 'www' Tenant.find_by!(subdomain: request.subdomain) # Przez nagłówek (dla API) elsif request.headers['X-Tenant-ID'].present? Tenant.find(request.headers['X-Tenant-ID']) # Przez użytkownika elsif current_user current_user.tenant end rescue ActiveRecord::RecordNotFound redirect_to root_url(subdomain: 'www'), alert: 'Tenant nie znaleziony' 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 # Administratorzy mogą należeć do wielu tenantów has_many :tenant_memberships has_many :accessible_tenants, through: :tenant_memberships, source: :tenant end ``` ```ruby # Na poziomie schematu z gemem Apartment (PostgreSQL) # config/initializers/apartment.rb Apartment.configure do |config| config.excluded_models = %w[Tenant User] config.tenant_names = -> { Tenant.pluck(:subdomain) } end # Użycie Apartment::Tenant.switch('acme') do # Wszystkie zapytania w tym bloku używają schematu 'acme' Project.all # SELECT * FROM acme.projects end ``` Poziom wiersza jest najprostszy, ale wymaga stałej uwagi pod kątem wycieków. Poziom schematu zapewnia lepszą izolację, ale komplikuje migracje. Wybór zależny od wymagań bezpieczeństwa i skalowalności. ### Pytanie 23: Jak skonfigurować architekturę mikroserwisową z Rails? Rails może służyć jako podstawa architektury mikroserwisowej z komunikacją przez HTTP/gRPC lub kolejki wiadomości. Kluczem jest właściwe zdefiniowanie granic. ```ruby # Klient serwisu HTTP # 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('Serwis niedostępny', code: response.code) end rescue Net::OpenTimeout, Net::ReadTimeout ServiceResult.failure('Timeout serwisu') end end ``` ```ruby # Komunikacja sterowana zdarzeniami z 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 # Wzorzec API Gateway # app/controllers/api/v1/gateway_controller.rb module Api module V1 class GatewayController < BaseController # Agregacja wielu serwisów 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} niedostępny", message: e.message } end end end end ``` Dla mikroserwisów Rails: definiować jasne kontrakty API (OpenAPI), implementować circuit breakers (gem `circuitbox`) i używać tracingu rozproszonego (gem `opentelemetry`). ### Pytanie 24: Jak wdrożyć aplikację Rails na produkcję? Nowoczesne wdrożenie Rails wykorzystuje kontenery lub PaaS. Solidna konfiguracja produkcyjna obejmuje assety, bazę danych i monitoring. ```ruby # config/environments/production.rb Rails.application.configure do config.cache_classes = true config.eager_load = true config.consider_all_requests_local = false # Assety 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 } # Wymuszenie SSL 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 # Obraz produkcyjny 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: ``` Lista kontrolna produkcji: obowiązkowy SSL, sekrety przez ENV, health checki, automatyczne backupy BD, monitoring (APM + logi + metryki) i skonfigurowane alerty. ### Pytanie 25: Jakie są nowości w Rails 7+, które warto znać? Rails 7+ wprowadza istotne zmiany: domyślne Hotwire, import maps, ulepszone szyfrowane credentials i wiele optymalizacji. ```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 "Załaduj więcej", articles_path(page: @page + 1), data: { turbo_frame: "articles" } %> <% end %> # Turbo Streams dla aktualizacji w czasie rzeczywistym # 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 # Kontrolery Stimulus # 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 (bez bundlera JavaScript) # 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" # Pin z 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 # Umożliwia wyszukiwanie encrypts :phone_number # Niedeterministyczne domyślnie 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 # Ulepszenia interfejsu zapytań # Rails 7.1+ # Zapytania asynchroniczne users = User.where(active: true).load_async # Kontynuować przetwarzanie podczas wykonywania zapytania # Dostęp do wyników przez 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') # Automatyczne wykrywanie inverse_of class Author < ApplicationRecord has_many :books # inverse_of wykrywane automatycznie end # Strict loading domyślnie (zapobiega N+1) class ApplicationRecord < ActiveRecord::Base self.strict_loading_by_default = true end ``` Rails 7+ stawia na prostotę (brak Webpack domyślnie) i HTML-over-the-wire z Hotwire. To podejście redukuje złożoność JavaScript, oferując nowoczesne doświadczenie użytkownika. ## Podsumowanie Rozmowy kwalifikacyjne z Ruby on Rails sprawdzają znajomość całego frameworka i zrozumienie jego konwencji. Kluczowe punkty do zapamiętania: ✅ **Podstawy**: MVC, Active Record, migracje, walidacje i powiązania ✅ **Architektura**: Service Objects, Concerns, Query Objects i wzorce CQRS ✅ **Wydajność**: zapytania N+1, caching (fragment, Russian Doll, low-level), eager loading ✅ **Testowanie**: RSpec, FactoryBot, request specs i dobre praktyki testowania ✅ **Bezpieczeństwo**: CSRF, SQL injection, XSS, Strong Parameters i uwierzytelnianie/autoryzacja ✅ **API**: projekt RESTful, JWT, serializatory i wersjonowanie ✅ **Produkcja**: zadania w tle, WebSockety, wdrożenia i monitoring Filozofia Rails (Convention over Configuration, DRY i Rails Way) kieruje wszystkimi decyzjami architektonicznymi. Opanowanie tych zasad i wiedza, kiedy od nich odejść, świadczy o solidnej ekspertyzie. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/pl/blog/ruby-on-rails/ruby-on-rails-interview-questions