Ruby on Rails Mulakat Sorulari: 2026 En Iyi 25

En cok sorulan 25 Ruby on Rails mulakat sorusu. MVC mimarisi, Active Record, migration, RSpec testi, REST API ayrintili cevaplar ve kod ornekleri ile.

Ruby on Rails Mulakat Sorulari - Tam Rehber

Ruby on Rails mülakatları, en popüler Ruby framework'üne hakimiyeti, MVC mimarisinin anlaşılmasını, Active Record ORM'sini ve "Convention over Configuration" felsefesine uygun sağlam web uygulamaları geliştirme yeteneğini değerlendirir. Bu rehber, Rails temellerinden ileri düzey üretim desenlerine kadar en sık sorulan 25 soruyu kapsar.

Mülakat ipucu

İşe alım uzmanları, Rails felsefesini anlayan adayları takdir eder: "Convention over Configuration", DRY (Don't Repeat Yourself) ve Rails Way kalıpları. Rails'in belirli mimari tercihleri neden yaptığını açıklamak fark yaratır.

Ruby on Rails temelleri

Soru 1: Ruby on Rails'te MVC desenini açıklayın

Model-View-Controller (MVC) deseni, Rails'in mimari çekirdeğidir. Sorumlulukları üç ayrı katmana bölerek kodun bakımını ve test edilebilirliğini iyileştirir.

ruby
# app/models/article.rb
# Model verileri ve iş mantığını yönetir
class Article < ApplicationRecord
  # Veri doğrulamaları
  validates :title, presence: true, length: { minimum: 5 }
  validates :body, presence: true

  # Diğer modellerle ilişkiler
  belongs_to :author, class_name: 'User'
  has_many :comments, dependent: :destroy
  has_many :tags, through: :article_tags

  # Yeniden kullanılabilir sorgular için scope'lar
  scope :published, -> { where(published: true) }
  scope :recent, -> { order(created_at: :desc).limit(10) }

  # Yaşam döngüsü callback'leri
  before_save :generate_slug

  private

  def generate_slug
    self.slug = title.parameterize if title_changed?
  end
end
ruby
# app/controllers/articles_controller.rb
# Controller istekleri alır ve yanıtı düzenler
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: 'Makale başarıyla oluşturuldu.'
    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 verileri HTML formatında gösterir %>
<article class="article-detail">
  <header>
    <h1><%= @article.title %></h1>
    <p class="meta">
      Yazan: <%= @article.author.name %>      <%= l @article.created_at, format: :long %>
    </p>
  </header>

  <div class="content">
    <%= simple_format @article.body %>
  </div>

  <%# Yorumlar için partial %>
  <%= render @comments %>
</article>

Tipik akış: istek Router'a ulaşır ve uygun Controller'a yönlendirilir. Controller verileri almak veya değiştirmek için Model ile etkileşime girer, ardından bu verileri HTML render için View'a aktarır.

Soru 2: Active Record nedir ve Rails ORM'si nasıl çalışır?

Active Record, Active Record desenini uygulayan Rails ORM'sidir (Object-Relational Mapping). Her Model sınıfı bir veritabanı tablosunu, her örnek ise bir satırı temsil eder.

ruby
# app/models/user.rb
# Active Record sütunları otomatik olarak niteliklere eşler
class User < ApplicationRecord
  # 'users' tablosu otomatik olarak ilişkilendirilir
  # Sütunlar: id, email, name, created_at, updated_at

  has_secure_password # Parola için BCrypt

  has_many :articles, foreign_key: :author_id
  has_one :profile, dependent: :destroy
  has_and_belongs_to_many :roles

  # Doğrulamalar
  validates :email, presence: true,
                    uniqueness: { case_sensitive: false },
                    format: { with: URI::MailTo::EMAIL_REGEXP }

  # Callback'ler
  before_save :normalize_email

  # Sorgular için sınıf metotları
  def self.admins
    joins(:roles).where(roles: { name: 'admin' })
  end

  private

  def normalize_email
    self.email = email.downcase.strip
  end
end
ruby
# Active Record sorgu örnekleri
# Rails konsolu veya bir servis içinde

# Oluşturma
user = User.create!(email: 'dev@example.com', name: 'Alice', password: 'secret123')

# Koşullarla okuma
active_users = User.where(active: true).order(:name)
user = User.find_by(email: 'dev@example.com')

# Zincirleme sorgular (lazy evaluation)
recent_admins = User.admins
                    .where('created_at > ?', 1.month.ago)
                    .includes(:profile)
                    .limit(10)

# Eager loading ile N+1 önleme
articles = Article.includes(:author, :comments).published

# Güncelleme
user.update!(name: 'Alice Martin')

# İşlemler
User.transaction do
  user.debit_balance!(100)
  recipient.credit_balance!(100)
  Payment.create!(from: user, to: recipient, amount: 100)
end

Active Record, Ruby metotlarını optimize edilmiş SQL sorgularına dönüştürür. where, joins, includes gibi metotlar tembeldir: sorgu yalnızca yineleme yapıldığında veya to_a çağrıldığında çalıştırılır.

Soru 3: Rails migration sistemini açıklayın

Migration'lar veritabanı şemasını Ruby ile sürümlendirmeye olanak tanır. Geri alınabilirler ve veri yapısının kontrollü gelişimine izin verirler.

ruby
# db/migrate/20260203100000_create_products.rb
# Tablo oluşturmak için migration
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 ve updated_at otomatik
    end

    # Performans için indeksler
    add_index :products, :name
    add_index :products, [:category_id, :active]
  end
end
ruby
# db/migrate/20260203110000_add_slug_to_products.rb
# Mevcut bir tabloyu değiştirmek için migration
class AddSlugToProducts < ActiveRecord::Migration[7.1]
  def change
    add_column :products, :slug, :string
    add_index :products, :slug, unique: true

    # Mevcut slug'ları doldur
    reversible do |dir|
      dir.up do
        Product.find_each do |product|
          product.update_column(:slug, product.name.parameterize)
        end
      end
    end

    # Doldurma sonrası null'a izin verme
    change_column_null :products, :slug, false
  end
end
bash
# Temel migration komutları
rails db:migrate              # Bekleyen migration'ları çalıştır
rails db:rollback             # Son migration'ı geri al
rails db:rollback STEP=3      # Son 3 migration'ı geri al
rails db:migrate:status       # Migration durumunu görüntüle
rails db:seed                 # db/seeds.rb'yi çalıştır
rails db:reset                # Drop, create, migrate, seed

Migration'lar geri alınabilir olmalıdır. change metodu akıllıdır ve yaygın işlemleri otomatik olarak tersine çevirebilir. Karmaşık durumlar için up ve down ayrı ayrı kullanılmalıdır.

İleri düzey Active Record

Soru 4: Rails'te N+1 sorgularını nasıl optimize edersiniz?

N+1 problemi, ilk sorgunun ardından ilişkileri yüklemek için N ek sorgu yapılmasıyla ortaya çıkar. Rails bu sorunu çözmek için çeşitli eager loading metotları sunar.

ruby
# app/controllers/orders_controller.rb
class OrdersController < ApplicationController
  def index
    # ❌ N+1 PROBLEMİ: 1 sorgu + sipariş başına N sorgu
    # @orders = Order.all
    # View'da: order.user.name sipariş başına bir sorgu üretir

    # ✅ ÇÖZÜM includes ile (eager loading)
    @orders = Order.includes(:user, :items)
                   .where(status: 'completed')
                   .order(created_at: :desc)
    # Toplamda yalnızca 3 sorgu üretir
  end

  def show
    # includes: ilişkileri ayrı yükler (2-3 sorgu)
    @order = Order.includes(items: :product).find(params[:id])

    # preload: ayrı yüklemeyi zorlar
    @order = Order.preload(:items, :user).find(params[:id])

    # eager_load: LEFT OUTER JOIN'i zorlar (1 sorgu)
    @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

  # Varsayılan includes ile scope
  scope :with_details, -> { includes(:user, items: :product) }

  # COUNT sorgularını önlemek için counter cache
  # Gerekli: add_column :users, :orders_count, :integer, default: 0
  belongs_to :user, counter_cache: true
end
ruby
# Bullet gem ile N+1 tespiti (geliştirme)
# config/environments/development.rb
config.after_initialize do
  Bullet.enable = true
  Bullet.alert = true
  Bullet.bullet_logger = true
  Bullet.rails_logger = true
end

# Bullet şu durumlarda uyarı verir:
# - Bir N+1 sorgusu tespit edildiğinde
# - Gereksiz eager loading olduğunda
# - Counter cache kullanılması gerektiğinde

Kural: varsayılan olarak includes kullanın (Rails optimal stratejiyi seçer), ayrı sorguları zorlamak istediğinizde preload, ilişkiler üzerinde filtreleme yaptığınızda eager_load.

Soru 5: Rails'te Scope'ları ve Query Object'leri açıklayın

Scope'lar yeniden kullanılabilir sorgu koşullarını kapsüller. Karmaşık sorgular için Query Object'ler daha iyi organizasyon ve test edilebilirlik sunar.

ruby
# app/models/product.rb
class Product < ApplicationRecord
  # Basit scope'lar
  scope :active, -> { where(active: true) }
  scope :in_stock, -> { where('stock_quantity > 0') }
  scope :featured, -> { where(featured: true) }

  # Parametreli scope'lar
  scope :cheaper_than, ->(price) { where('price < ?', price) }
  scope :in_category, ->(category) { where(category: category) }

  # Zincirlenebilir scope'lar
  scope :available, -> { active.in_stock }

  # Joins ile scope
  scope :with_recent_orders, -> {
    joins(:order_items)
      .where('order_items.created_at > ?', 30.days.ago)
      .distinct
  }

  # Alt sorgu ile scope
  scope :bestsellers, -> {
    where(id: OrderItem.group(:product_id)
                       .order('COUNT(*) DESC')
                       .limit(10)
                       .select(:product_id))
  }
end
ruby
# app/queries/products_search_query.rb
# Karmaşık aramalar için Query Object
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

# Controller'da kullanım
@products = ProductsSearchQuery.new(Product.active).call(params)

Scope'lar basit, yeniden kullanılabilir koşullar için mükemmeldir. Query Object'ler birden fazla isteğe bağlı filtre ve kompozisyon mantığı içeren karmaşık aramalar için uygundur.

Ruby on Rails mülakatlarında başarılı olmaya hazır mısın?

İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.

Routing ve Controller'lar

Soru 6: Rails'te RESTful routing nasıl çalışır?

Rails, HTTP fiillerini CRUD eylemlerine eşleyen RESTful rotaları teşvik eder. Router URL'leri belirli controller çağrılarına çevirir.

ruby
# config/routes.rb
Rails.application.routes.draw do
  # Standart RESTful rotalar (7 eylem)
  resources :articles do
    # İç içe rotalar
    resources :comments, only: [:create, :destroy]

    # Member rotalar (bir örnek üzerinde işlem yapar)
    member do
      post :publish
      delete :archive
    end

    # Collection rotalar (koleksiyon üzerinde işlem yapar)
    collection do
      get :drafts
      get :search
    end
  end

  # Namespace ile API rotaları
  namespace :api do
    namespace :v1 do
      resources :products, only: [:index, :show, :create, :update] do
        resources :reviews, shallow: true
      end
    end
  end

  # Özel rota
  get 'dashboard', to: 'dashboard#index'

  # Rota kısıtlamaları
  constraints(SubdomainConstraint.new) do
    resources :admin_settings
  end

  # Kök rotası
  root 'home#index'
end
bash
# rails routes - Üretilen tüm rotaları gösterir
#
# 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

Üretilen rota yardımcıları (article_path(@article), new_article_path) URL'lere dinamik ve sürdürülebilir şekilde başvurmayı sağlar.

Soru 7: Controller'larda callback'leri ve filtreleri açıklayın

Callback'ler (before_action, after_action, around_action) controller eylemlerinden önce, sonra veya etrafında kod çalıştırmaya olanak tanır.

ruby
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  # CSRF koruması varsayılan olarak etkin
  protect_from_forgery with: :exception

  # Kimlik doğrulama için global callback
  before_action :authenticate_user!

  # Genel hata yönetimi
  rescue_from ActiveRecord::RecordNotFound, with: :not_found
  rescue_from ActionController::ParameterMissing, with: :bad_request

  private

  def not_found
    render json: { error: 'Kaynak bulunamadı' }, 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
  # Seçeneklerle callback'ler
  before_action :require_admin
  before_action :set_product, only: [:show, :edit, :update, :destroy]
  after_action :log_activity, only: [:create, :update, :destroy]

  # Koşullu 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: 'Ürün oluşturuldu.'
    else
      render :new, status: :unprocessable_entity
    end
  end

  def update
    if @product.update(product_params)
      redirect_to [:admin, @product], notice: 'Ürün güncellendi.'
    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

Callback'ler bildirim sırasına göre çalışır. Miras alınan callback'leri devre dışı bırakmak için alt sınıflarda skip_before_action kullanın. Çok fazla iş mantığı içeren callback'lerden kaçının: Service Object'leri tercih edin.

Servisler ve mimari

Soru 8: Rails'te Service Object'leri nasıl uygulanır?

Service Object'ler Modellere veya Controller'lara ait olmayan karmaşık iş mantığını kapsüller. Test edilebilirliği artırır ve tek sorumluluk ilkesine uyar.

ruby
# app/services/order_processor.rb
# Standart arayüze sahip Service Object
class OrderProcessor
  def initialize(order, payment_method:)
    @order = order
    @payment_method = payment_method
  end

  def call
    return failure('Sipariş zaten işlendi') 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("Ödeme başarısız: #{e.message}")
  rescue InsufficientStockError => e
    failure("Stok yetersiz: #{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: "Sipariş ##{@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: 'Sipariş onaylandı!'
      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

Service Object kalıbı basit bir kuralı izler: bir sınıf, bir sorumluluk, bir genel call metodu. Bir Result nesnesi döndürmek başarı ve başarısızlığı temiz biçimde ele almayı sağlar.

Soru 9: Rails'te Concern'leri açıklayın

Concern'ler Modeller veya Controller'lar arasında kod çıkarmayı ve paylaşmayı sağlar. Temiz bir dahil etme sözdizimi için ActiveSupport::Concern kullanırlar.

ruby
# app/models/concerns/sluggable.rb
# Slug üretmek için yeniden kullanılabilir Concern
module Sluggable
  extend ActiveSupport::Concern

  included do
    # Dahil edildiğinde çalışan kod
    before_validation :generate_slug, if: :should_generate_slug?
    validates :slug, presence: true, uniqueness: true
  end

  # Sınıf metotları
  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

  # Örnek metotları
  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 # Opsiyonel, varsayılan :title
end

# app/models/product.rb
class Product < ApplicationRecord
  include Sluggable

  sluggable_source :name
end
ruby
# app/controllers/concerns/pagination.rb
# Controller'lar için Concern
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

Concern'ler gerçekten paylaşılan kod için kullanışlıdır. Bir Modeli yalnızca "kısaltmak" için Concern oluşturmaktan kaçının: bu, karmaşıklığı azaltmadan gizler.

RSpec ile test

Soru 10: Rails'te RSpec testleri nasıl yapılandırılır?

RSpec, Rails için standart test çerçevesidir. İyi bir test yapısı Model spec'leri, Controller spec'leri, Service spec'leri ve entegrasyon testlerini içerir.

ruby
# spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do
  # FactoryBot ile fabrikalar
  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 'e-posta formatını doğrular' 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 'ad ve soyadı birleştirerek döndürür' do
      user = build(:user, first_name: 'John', last_name: 'Doe')
      expect(user.full_name).to eq('John Doe')
    end

    it 'eksik soyadını ele alır' do
      user = build(:user, first_name: 'John', last_name: nil)
      expect(user.full_name).to eq('John')
    end
  end

  describe '.active' do
    it 'yalnızca aktif kullanıcıları döndürür' 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 'sipariş geçerli olduğunda' do
      before do
        allow(PaymentGateway).to receive(:charge).and_return(
          OpenStruct.new(success?: true, transaction_id: 'txn_123')
        )
      end

      it 'siparişi başarıyla işler' do
        result = subject.call

        expect(result).to be_success
        expect(order.reload.status).to eq('completed')
      end

      it 'ürün stoğunu azaltır' do
        expect { subject.call }.to change { product.reload.stock_quantity }.by(-2)
      end

      it 'onay e-postası gönderir' do
        expect { subject.call }
          .to have_enqueued_mail(OrderMailer, :confirmation)
          .with(order)
      end
    end

    context 'ödeme başarısız olduğunda' do
      before do
        allow(PaymentGateway).to receive(:charge).and_return(
          OpenStruct.new(success?: false, error: 'Card declined')
        )
      end

      it 'başarısız sonuç döndürür' do
        result = subject.call

        expect(result).to be_failure
        expect(result.error).to include('Card declined')
      end

      it 'sipariş durumunu güncellemez' 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 'ürün listesini döndürür' do
      get '/api/v1/products', headers: headers

      expect(response).to have_http_status(:ok)
      expect(json_response['data'].size).to eq(3)
    end

    it 'kategoriye göre filtreler' 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: 'Yeni Ürün', price: 99.99, category_id: create(:category).id } }
    end

    it 'yeni bir ürün oluşturur' 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

İyi uygulamalar: veriler için let, metotlar/bağlamlar için describe, koşullar için context ve belirli iddialar için it kullanın. Her test tek bir şeyi test etmelidir.

Soru 11: FactoryBot ile fabrikaları nasıl kullanılır?

FactoryBot, test verilerini bildirimsel ve sürdürülebilir biçimde oluşturmaya olanak tanır. Fabrikalar statik fixture'ların yerini alır.

ruby
# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    # Benzersizlik için sıralamalar
    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 }

    # Varyasyonlar için trait'ler
    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

    # Miras alınan fabrika
    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
# Testlerde kullanım
RSpec.describe OrderProcessor do
  # build: kalıcı olmayan örnek
  let(:user) { build(:user) }

  # create: VT'ye kaydedilmiş
  let(:order) { create(:order, :with_items, user: user) }

  # create_list: birden fazla örnek
  let(:products) { create_list(:product, 5) }

  # Trait'leri birleştirme
  let(:admin) { create(:user, :admin, :with_profile) }

  # Nitelikleri geçersiz kılma
  let(:expensive_order) { create(:order, :with_items, items_count: 10) }

  # build_stubbed: daha hızlı, birim testleri için
  let(:stubbed_user) { build_stubbed(:user) }
end

Kalıcılık gerekmediğinde create yerine build veya build_stubbed tercih edin: bu, testleri önemli ölçüde hızlandırır.

Arka plan işleri

Soru 12: Rails'te Active Job ve Sidekiq nasıl kullanılır?

Active Job, arka plan işleri için backend'den (Sidekiq, Resque vb.) bağımsız birleşik bir arayüz sunar. Sidekiq, Redis ile performansı sayesinde popüler tercihtir.

ruby
# app/jobs/process_order_job.rb
class ProcessOrderJob < ApplicationJob
  queue_as :default

  # Yeniden deneme yapılandırması
  retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3
  retry_on Net::OpenTimeout, wait: :polynomially_longer, attempts: 10
  discard_on ActiveJob::DeserializationError

  # Sidekiq seçenekleri (Sidekiq backend ise)
  sidekiq_options retry: 5, backtrace: true

  def perform(order_id)
    order = Order.find(order_id)

    OrderProcessor.new(order).call
  rescue ActiveRecord::RecordNotFound
    # Sıraya alma ile yürütme arasında sipariş silinmiş
    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

  # Sidekiq Enterprise veya throttle gem ile hız sınırlama
  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
# İşleri sıraya alma
# Anında
ProcessOrderJob.perform_later(order.id)

# Geciktirilmiş
ProcessOrderJob.set(wait: 5.minutes).perform_later(order.id)

# Belirli bir saatte
ProcessOrderJob.set(wait_until: Date.tomorrow.noon).perform_later(order.id)

# Belirli kuyruk
ProcessOrderJob.set(queue: :critical).perform_later(order.id)

# Senkron (test veya hata ayıklama için)
ProcessOrderJob.perform_now(order.id)
ruby
# config/sidekiq.yml
:concurrency: 10
:queues:
  - [critical, 3]    # Yüksek öncelik, ağırlık 3
  - [default, 2]     # Orta öncelik, ağırlık 2
  - [mailers, 1]     # Düşük öncelik, ağırlık 1
  - [low, 1]

:schedule:
  cleanup_job:
    cron: '0 3 * * *'  # Her gün 03:00
    class: CleanupJob

Active Job backend'i soyutlar ancak belirli özelliklere (batch'ler, hız sınırlama) erişim genellikle seçilen backend'e bağımlılığı gerektirir.

Ruby on Rails mülakatlarında başarılı olmaya hazır mısın?

İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.

API geliştirme

Soru 13: Rails ile RESTful API nasıl oluşturulur?

Rails, API-only Controller'lar ve serializer'larla JSON API oluşturmayı kolaylaştırır. İyi bir API sürümlendirilmiş, belgelenmiş ve güvenlidir.

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: 'Kaynak bulunamadı', details: exception.message },
               status: :not_found
      end

      def unprocessable_entity(exception)
        render json: { error: 'Doğrulama başarısız', details: exception.record.errors },
               status: :unprocessable_entity
      end

      def bad_request(exception)
        render json: { error: 'Geçersiz istek', 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
# jsonapi-serializer gem ile
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 iyi uygulamaları: namespace ile sürümlendir, uygun HTTP kodlarını kullan, koleksiyonları sayfalandır ve net hata mesajları sun.

Soru 14: Rails'te JWT kimlik doğrulaması nasıl uygulanır?

JWT (JSON Web Tokens), API'ler için popüler bir durumsuz kimlik doğrulama yöntemidir. Token, kullanıcı kimliğini ve geçerliliğini kodlar.

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 süresi doldu'
    rescue JWT::DecodeError
      raise AuthenticationError, 'Geçersiz 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: 'Geçersiz kimlik bilgileri' }, 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 eksik' 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: 'Kullanıcı bulunamadı' }, status: :unauthorized
  end

  def current_user
    @current_user
  end
end

Üretim için göz önünde bulundurun: refresh token'lar, çıkış sonrasında token'ları kara listeye alma ve kısa son kullanma süreleri. devise-jwt gibi gem'ler uygulamayı basitleştirir.

Önbellekleme ve performans

Soru 15: Rails'te önbelleklemeyi nasıl uygularız?

Rails çeşitli önbellekleme seviyeleri sunar: fragment caching, Russian Doll caching, low-level caching. Seçim kullanım durumuna bağlıdır.

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 %>
<%# Otomatik önbellek anahtarıyla fragment caching %>
<% @products.each do |product| %>
  <%# Ürünün updated_at'ine dayalı önbellek %>
  <% cache product do %>
    <%= render product %>
  <% end %>
<% end %>

<%# Russian Doll caching - iç içe önbellek %>
<% cache ['v1', @category] do %>
  <h2><%= @category.name %></h2>

  <% @category.products.each do |product| %>
    <% cache ['v1', product] do %>
      <%= render product %>
    <% end %>
  <% end %>
<% end %>

<%# Koşullu önbellek %>
<% cache_if current_user.nil?, @product do %>
  <%= render @product %>
<% end %>
ruby
# app/models/product.rb
class Product < ApplicationRecord
  # Russian Doll önbelleğini geçersiz kılmak için ebeveyne touch
  belongs_to :category, touch: true

  # Özel önbellek anahtarı
  def cache_key_with_version
    "#{super}/#{reviews.maximum(:updated_at)&.to_i}"
  end
end
ruby
# Servislerde low-level caching
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

# Race condition korumalı önbellek
Rails.cache.fetch('popular_products', expires_in: 1.hour, race_condition_ttl: 10.seconds) do
  Product.bestsellers.limit(10).to_a
end

# Açık geçersiz kılma
Rails.cache.delete('dashboard:stats')
Rails.cache.delete_matched('products:*')

Russian Doll caching etkilidir çünkü yalnızca değiştirilen parçalar yeniden üretilir. Geçersizliği yaymak için ilişkilerde touch: true kullanın.

Soru 16: Bir Rails uygulamasının performansı nasıl optimize edilir?

Rails optimizasyonu birden çok yönü kapsar: VT sorguları, önbellekleme, asset'ler ve mimari. İzleme ile metodik bir yaklaşım esastır.

ruby
# Veritabanı optimizasyonu
# config/database.yml
production:
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  prepared_statements: true
  advisory_locks: true

# app/models/order.rb
class Order < ApplicationRecord
  # Sık sorgular için bileşik indeksler
  # add_index :orders, [:user_id, :status, :created_at]

  # Yalnızca gerekli sütunları seç
  scope :summary, -> { select(:id, :status, :total, :created_at) }

  # Büyük hacimler için toplu işleme
  def self.process_pending
    pending.find_each(batch_size: 1000) do |order|
      ProcessOrderJob.perform_later(order.id)
    end
  end

  # Tekrarlayan hesaplamalardan kaçınma
  def self.revenue_by_month
    completed
      .group("DATE_TRUNC('month', created_at)")
      .sum(:total)
  end
end
ruby
# Bellek optimizasyonu
# 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
# rack-mini-profiler ile profil oluşturma
# 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
# Tembel yükleme ve sayfalama
class ProductsController < ApplicationController
  def index
    @products = Product.active
                       .includes(:category, :primary_image)
                       .page(params[:page])
                       .per(24)

    # Sonraki sayfa için ön yükleme
    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

Temel araçlar: profil oluşturmak için rack-mini-profiler, N+1 tespiti için bullet, üretim izleme için New Relic veya Scout.

Güvenlik

Soru 17: Rails'te güvenlik için en iyi uygulamalar nelerdir?

Rails, yaygın güvenlik açıklarına karşı varsayılan korumalar içerir. Bu korumaları anlamak ve doğru yapılandırmak çok önemlidir.

ruby
# CSRF koruması
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  # Varsayılan olarak etkin, geçersiz token'da exception fırlatır
  protect_from_forgery with: :exception

  # API'ler için :null_session kullanın
  # protect_from_forgery with: :null_session
end

# View'larda token formlara otomatik olarak eklenir
# <%= form_with ... %> authenticity_token içerir

# AJAX istekleri için
# csrf_meta_tags değeriyle X-CSRF-Token header'ı ekleyin
ruby
# SQL Injection önleme
# ✅ Enterpole edilmiş parametreler otomatik olarak escape edilir
User.where('email = ?', params[:email])
User.where(email: params[:email])

# ❌ TEHLİKE - Doğrudan interpolasyon
User.where("email = '#{params[:email]}'")

# ✅ Dinamik ORDER yan tümceleri için
ALLOWED_SORTS = %w[name created_at price].freeze
sort_column = ALLOWED_SORTS.include?(params[:sort]) ? params[:sort] : 'name'
Product.order(sort_column)
ruby
# XSS koruması
# Rails view'larda HTML'i otomatik olarak escape eder

# ✅ Otomatik escape
<%= user.name %>

# ❌ Tehlikeli - escape edilmemiş içerik
<%== user.bio %>
<%= raw user.bio %>
<%= user.bio.html_safe %>

# ✅ Güvenli HTML için sanitize kullanın
<%= 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
    # İzin verilen niteliklerin açık beyaz listesi
    params.require(:user).permit(:name, :email, :avatar)

    # Yalnızca yöneticiler için
    if current_user.admin?
      params.require(:user).permit(:name, :email, :role, :active)
    else
      params.require(:user).permit(:name, :email)
    end
  end
end
ruby
# Güvenlik header'ları
# 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

brakeman (statik güvenlik analizi) ile düzenli denetim yapın ve bundle audit ile gem'leri güncel tutun.

Soru 18: Rails'te kimlik doğrulama ve yetkilendirme nasıl ele alınır?

Kimlik doğrulama kimliği doğrular, yetkilendirme izinleri kontrol eder. Devise auth'u, Pundit veya CanCanCan yetkilendirmeyi yönetir.

ruby
# Devise kurulumu
# 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 politikaları
# 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

  # Koleksiyonlar için scope
  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
# Pundit ile controller
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: 'Makale güncellendi.'
    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: 'Makale yayınlandı.'
  end

  rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized

  private

  def user_not_authorized
    flash[:alert] = "Bu eylemi gerçekleştirme yetkiniz yok."
    redirect_back(fallback_location: root_path)
  end
end

Pundit, CanCanCan'a göre daha açık ve test edilebilirdir. Her eylemin karşılık gelen bir politika metodu vardır ve scope'lar koleksiyonları otomatik olarak filtreler.

İleri düzey Rails

Soru 19: Rails'te Repository desenini açıklayın

Repository deseni veri erişim mantığını uygulamanın geri kalanından izole eder. Rails Active Record kullansa da (farklı bir desen), Repository karmaşık durumlarda yararlı olabilir.

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
# Bir serviste kullanım
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

# Mock'larla test etmeyi kolaylaştırır
RSpec.describe ProductSearchService do
  let(:repository) { instance_double(ProductRepository) }
  let(:service) { described_class.new(repository: repository) }

  it 'kategoriye göre filtreler' 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

Active Record zaten harika bir desen olduğu için Repository Rails'te isteğe bağlıdır. Karmaşık sorgular veya depolama izolasyonu önemli olduğunda kullanın.

Soru 20: Rails'te CQRS deseni nasıl uygulanır?

CQRS (Command Query Responsibility Segregation) okuma ve yazma işlemlerini ayırır. Rails'te bu, query'ler ve command'lar için ayrı sınıflara dönüşür.

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, "Ürün #{item[:product_id]} mevcut değil")
        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
# CQRS kullanan controller
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: 'Sipariş oluşturuldu!'
    else
      flash.now[:alert] = result.errors.join(', ')
      render :new, status: :unprocessable_entity
    end
  end
end

CQRS, asimetrik okuma/yazma ihtiyaçları olan karmaşık uygulamalarda parlar. Basit CRUD için aşırı mühendisliktir.

Soru 21: Action Cable ile WebSocket nasıl ele alınır?

Action Cable, gerçek zamanlı çift yönlü iletişim için Rails'e WebSocket'leri entegre eder. Sunucular arası senkronizasyon için Redis kullanır.

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
      # Oturum çerezi üzerinden
      if verified_user = User.find_by(id: cookies.encrypted[:user_id])
        verified_user
      # API'ler için JWT üzerinden
      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])

    # İzinleri kontrol et
    unless @room.accessible_by?(current_user)
      reject
      return
    end

    stream_for @room

    # Diğerlerine varlığı bildir
    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']
    )

    # Tüm abonelere yayınla
    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
app/javascript/channels/chat_channel.jsjavascript
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 yeniden bağlanmaları ve senkronizasyonu otomatik olarak yönetir. Üretimde Redis'i adapter olarak yapılandırın ve eşzamanlı bağlantılara göre ölçeklendirin.

Soru 22: Rails'te multi-tenancy nasıl uygulanır?

Multi-tenancy, bir uygulamanın birden fazla izole müşteriye (tenant) hizmet vermesini sağlar. Üç ana yaklaşım: veritabanı, şema veya satır düzeyi.

ruby
# ActsAsTenant veya manuel ile satır düzeyi multitenancy
# app/models/concerns/tenant_scoped.rb
module TenantScoped
  extend ActiveSupport::Concern

  included do
    belongs_to :tenant

    # Geçerli tenant'a varsayılan scope
    default_scope -> { where(tenant: Current.tenant) if Current.tenant }

    # Tenant doğrulama
    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
    # Alt alan adı üzerinden
    if request.subdomain.present? && request.subdomain != 'www'
      Tenant.find_by!(subdomain: request.subdomain)
    # Header üzerinden (API'ler için)
    elsif request.headers['X-Tenant-ID'].present?
      Tenant.find(request.headers['X-Tenant-ID'])
    # Kullanıcı üzerinden
    elsif current_user
      current_user.tenant
    end
  rescue ActiveRecord::RecordNotFound
    redirect_to root_url(subdomain: 'www'), alert: 'Tenant bulunamadı'
  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

  # Yöneticiler birden fazla tenant'a ait olabilir
  has_many :tenant_memberships
  has_many :accessible_tenants, through: :tenant_memberships, source: :tenant
end
ruby
# Apartment gem ile şema düzeyinde (PostgreSQL)
# config/initializers/apartment.rb
Apartment.configure do |config|
  config.excluded_models = %w[Tenant User]
  config.tenant_names = -> { Tenant.pluck(:subdomain) }
end

# Kullanım
Apartment::Tenant.switch('acme') do
  # Bu bloktaki tüm sorgular 'acme' şemasını kullanır
  Project.all # SELECT * FROM acme.projects
end

Satır düzeyi en basit olanıdır ancak sızıntılara karşı sürekli dikkat gerektirir. Şema düzeyi daha iyi izolasyon sunar ancak migration'ları karmaşıklaştırır. Güvenlik ve ölçeklenebilirlik ihtiyaçlarına göre seçim yapın.

Soru 23: Rails ile mikroservis mimarisi nasıl kurulur?

Rails, HTTP/gRPC veya mesaj kuyrukları üzerinden iletişimle mikroservis mimarisinin temeli olarak hizmet edebilir. Anahtar, sınırları iyi tanımlamaktır.

ruby
# HTTP servis istemcisi
# 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('Servis kullanılamıyor', code: response.code)
    end
  rescue Net::OpenTimeout, Net::ReadTimeout
    ServiceResult.failure('Servis zaman aşımı')
  end
end
ruby
# Sidekiq/Redis ile olay tabanlı iletişim
# 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 deseni
# app/controllers/api/v1/gateway_controller.rb
module Api
  module V1
    class GatewayController < BaseController
      # Birden fazla servisi birleştir
      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} kullanılamıyor", message: e.message }
      end
    end
  end
end

Rails mikroservisleri için: net API sözleşmeleri (OpenAPI) tanımlayın, devre kesiciler uygulayın (gem circuitbox) ve dağıtık izleme kullanın (gem opentelemetry).

Soru 24: Bir Rails uygulaması üretime nasıl deploy edilir?

Modern Rails dağıtımı container'ları veya PaaS'ı kullanır. Sağlam bir üretim yapılandırması asset'leri, veritabanını ve izlemeyi kapsar.

ruby
# config/environments/production.rb
Rails.application.configure do
  config.cache_classes = true
  config.eager_load = true
  config.consider_all_requests_local = false

  # Asset'ler
  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) }

  # Önbellek
  config.cache_store = :redis_cache_store, {
    url: ENV['REDIS_URL'],
    expires_in: 1.day
  }

  # SSL'i zorla
  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

# Üretim imajı
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:

Üretim kontrol listesi: zorunlu SSL, ENV ile sırlar, sağlık kontrolleri, otomatik VT yedekleri, izleme (APM + log + metrik) ve yapılandırılmış uyarılar.

Soru 25: Rails 7+'ta bilinmesi gereken yeni özellikler nelerdir?

Rails 7+ önemli değişiklikler getirir: varsayılan Hotwire, import map'ler, geliştirilmiş şifreli credentials ve birçok optimizasyon.

ruby
# Hotwire - Turbo Frame'ler
# 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 "Daha fazla yükle", articles_path(page: @page + 1),
              data: { turbo_frame: "articles" } %>
<% end %>

# Gerçek zamanlı güncellemeler için Turbo Stream'ler
# app/controllers/comments_controller.rb
def create
  @comment = @article.comments.create!(comment_params.merge(user: current_user))

  respond_to do |format|
    format.turbo_stream
    format.html { redirect_to @article }
  end
end

# app/views/comments/create.turbo_stream.erb
<%= turbo_stream.append "comments", @comment %>
<%= turbo_stream.update "comments_count", @article.comments.count %>
<%= turbo_stream.replace "comment_form", partial: "comments/form", locals: { comment: Comment.new } %>
ruby
# Stimulus controller'ları
# 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 Map (JavaScript bundler olmadan)
# 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"

# CDN'den pin'ler
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  # Aramaya izin verir
  encrypts :phone_number                 # Varsayılan olarak deterministik değil
  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
# Sorgu arayüzünde iyileştirmeler
# Rails 7.1+

# Asenkron sorgular
users = User.where(active: true).load_async
# Sorgu çalışırken işlemeye devam et
# Sonuçlara users.to_a ile erişin

# 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')

# Otomatik inverse_of tespiti
class Author < ApplicationRecord
  has_many :books # inverse_of otomatik tespit edilir
end

# Varsayılan strict loading (N+1'i önler)
class ApplicationRecord < ActiveRecord::Base
  self.strict_loading_by_default = true
end

Rails 7+ basitliği (varsayılan olarak Webpack yok) ve Hotwire ile HTML-over-the-wire'ı tercih eder. Bu yaklaşım, modern bir kullanıcı deneyimi sunarken JavaScript karmaşıklığını azaltır.

Sonuç

Ruby on Rails mülakatları, framework'ün tamamına hakimiyeti ve sözleşmelerinin anlaşılmasını değerlendirir. Hatırlanması gereken temel noktalar:

Temeller: MVC, Active Record, migration'lar, doğrulamalar ve ilişkiler

Mimari: Service Object'ler, Concern'ler, Query Object'ler ve CQRS desenleri

Performans: N+1 sorguları, caching (fragment, Russian Doll, low-level), eager loading

Test: RSpec, FactoryBot, request spec'ler ve test iyi uygulamaları

Güvenlik: CSRF, SQL injection, XSS, Strong Parameters ve kimlik doğrulama/yetkilendirme

API'ler: RESTful tasarım, JWT, serializer'lar ve sürümlendirme

Üretim: arka plan işleri, WebSocket'ler, dağıtım ve izleme

Rails felsefesi (Convention over Configuration, DRY ve Rails Way) tüm mimari kararlara rehberlik eder. Bu prensiplere hakim olmak ve ne zaman onlardan ayrılınacağını bilmek sağlam bir uzmanlığı gösterir.

Pratik yapmaya başla!

Mülakat simülatörleri ve teknik testlerle bilgini test et.

Etiketler

#ruby on rails
#ruby
#mulakat
#active record
#teknik mulakat

Paylaş

İlgili makaleler