2 Nisan 2025 Çarşamba

Rails 7 Denemeler 5

https://ujk-ujk.blogspot.com/2025/04/rails-7-denemeler-5.html
İçindekiler +


İletişim sayfası

Uygulamamıza İletişim sayfası ekleyerek devam edeceğiz. Öncelikle test rutinlerini ekleyelim. 

test/controllers/static_pages_controller_test.rb

require "test_helper"

class StaticPagesControllerTest < ActionDispatch::IntegrationTest
  ....

  test "should get contact" do
    get static_pages_contact_url
    assert_response :success
    assert_select "title", "İletişim | Yeni App"
  end

end


Tabi ki test yaparsak hata verecektir.

$ rails t


Aynı daha önce about eylemi için yaptıklarımızı tekrarlayacağız. Öncelikle yönlendirmesini yapalım.

config/routes.rb

Rails.application.routes.draw do
  get "static_pages/home"
  get "static_pages/help"
  get "static_pages/about"
  get "static_pages/contact"
  ....
end


Şimdi de kontrolöre eylemi ekleyelim.

app/controllers/static_pages_controller.rb

class StaticPagesController < ApplicationController
  def home
  end

  def help
  end

  def about
  end

  def contact
  end
end


Şimdi bir de görsel sayfası ekleyelim.

app/views/static_pages/contact.html.erb

<% provide(:title, 'İletişim') %>
<h1>İletişim</h1>
<p>
 Yeni App uygulamamız için iletişimi konunun anlatıldığı
 <a href="https://ujk-ujk.blogspot.com/2025/03/rails-7-denemeler-1.html">
 blog sayfalarında</a> yorum yaparak bulabilirsiniz.
</p>


Artık uygulamamız testten geçiyor durumda. İletişim sayfası eklendi. 




Rails yönlendirmeler

İsimlendirilmiş yönlendirmeler için tabi ki uygulamamızın yönlendirmelerinin yapıldığı config/routes.rb dosyası kullanılır. 

  root "static_pages#home"

Bu satır uygulamamızın / ana root adresini static_pages#home eylemine yönlendiriyor. Bu şekil yönlendirmenin bir önemli etkisi daha var. Yönlendirmeleri düz URL değeri yerine isimlendirilmiş yönlendirmelerle yapmak. Uygulamamızda kullanabileceğimiz iki isimlendirilmiş yönlendirme var, root_path ve root_url değişkenleri.

root_path -> '/'
root_url -> 'http://www.example.com/'

path değişken uygulama root'una göre bağıl, url değişken ise tam url web adresi olur. Normalde redirect hariç path değişkenler kullanacağız. 

Şimdi diğer sayfalar için de isimlendirilmiş yönlendirme yapmak için routes.rb dosyamızı değiştirelim. 

  get "static_pages/help"

bunun yerine 

  get "/help", to: "static_pages#help"

yapmalıyız. Bu sayede help_path ve help_url değişkenleri oluşur. Hepsi için uygularsak

config/routes.rb

Rails.application.routes.draw do
  root "static_pages#home"
  get "/help", to: "static_pages#help"
  get "/about", to: "static_pages#about"
  get "/contact", to: "static_pages#contact"
.....


Yönlendirmeler değiştiği için testlerimiz hata verecektir. Onları da bir düzenleyelim.

static_pages_controller_test.rb

require "test_helper"

class StaticPagesControllerTest < ActionDispatch::IntegrationTest
  test "should get home" do
    get root_path
    assert_response :success
    assert_select "title", "Yeni App"
  end

  test "should get help" do
    get help_path
    assert_response :success
    assert_select "title", "Yardım | Yeni App"
  end

  test "should get about" do
    get about_path
    assert_response :success
    assert_select "title", "Hakkımızda | Yeni App"
  end

  test "should get contact" do
    get contact_path
    assert_response :success
    assert_select "title", "İletişim | Yeni App"
  end

end




İsimlendirilmiş yönlendirmeleri kullanalım

Yönlendirmeleri düzenleyerek artık görsellerimizdeki linklerde isimlendirilmiş yönlendirmeleri kullanabiliriz. link_to metodlarına verdiğimiz ikinci argümanlardaki "#" değerini artık değiştirebiliriz. 

<%= link_to "Hakkımızda", "#" %>

yerine

<%= link_to "Hakkımızda", about_path %>

yazabiliriz. 

Kısmi görselimiz _header.html.erb görselinden başlayalım.

app/views/layouts/_header.html.erb

<header class="navbar navbar-fixed-top navbar-inverse">
  <div class="container">
    <%= link_to "Yeni App", "#", id: "logo" %>
    <nav>
      <ul class="nav navbar-nav navbar-right">
        <li><%= link_to "Ana Sayfa", root_path %></li>
        <li><%= link_to "Yardım", help_path %></li>
        <li><%= link_to "Giriş Yap", "#" %></li>
      </ul>
    </nav>
  </div>
</header>


Sadece Giriş Yap linkini sonraya bıraktık, çünkü daha şifreli kullanıcı girişleri konusuna çok var. Ana sayfa için root_path ve yardım sayfası için help_path değişkenlerini kullanıyoruz. 

Sırada ikinci kısmi görselimiz _footer.html.erb dosyası var.

app/views/layouts/_footer.html.erb

<footer class="footer">
  <a href="https://rubyonrails.org/">Ruby On Rails</a>
  <nav>
    <ul>
      <li><%= link_to "Hakkımızda", about_path %></li>
      <li><%= link_to "İletişim", contact_path %></li>
      <li><%= link_to "Haberler", "https://rubyonrails.org/blog/",
        target: "_blank" %></li>
    </ul>
  </nav>
</footer>


Burada da Hakkımızda sayfası için about_path ve İletişim sayfası için contact_path değişkenlerini kullanıyoruz. Burada Haberler linkinde de target: "_blank" öznitelik değerini verdik, <a> elemanlarında target="_blank" özelliği bağlantının yeni sekmede açılmasını sağlar. Burada da Haberler bağlantısı bir dış siteye gönderdiği için yeni bir sekmede açılmasını sağladık. 



Yerleşimdeki linkleri test etmek

Yerleşim görsel dosyamızdaki linkleri değiştirdik, şimdi bir test kodu yazarak sağlıklı çalıştıklarını görelim. Bunu tarayıcımızda linkleri tek tek tıklayarak deneyebiliriz. Ama burada amacımız üzüm yemek değil, test işlerini öğrenmeye çalışıyoruz. 

Bu adımda bir integration_test üreterek başlayacağız. Bunlar uygulamamızın davranışları konusunda uçtan uca testler yazmamıza yardımcı olur. Adını site_layout koyduğumuz bir test şablonunu Rails'e ürettirmek için terminalde şu komutu girelim.

$ rails g integration_test site_layout
      invoke  test_unit
      create    test/integration/site_layout_test.rb

rails g komutunun rails generate komutu ile aynı işi yaptığını söylemiş miydim? Dağlara taşlara bir komut, üretmediği şey yok.

Dikkat ettiyseniz Rails ürettiği test dosyasının adına otomatik olarak _test ilavesi getirdi. 

Planımız şu:

  • Ana sayfayı çağırmak
  • Doğru görsel şablonuın yayınlandığından emin olmak
  • Ana Sayfa, Yardım, Hakkımızda ve İletişim linklerinin doğruluğunu görmek

test/integration/site_layout_test.rb

require "test_helper"

class SiteLayoutTest < ActionDispatch::IntegrationTest
  test "layout links" do
    get root_path
    assert_template "static_pages/home"
    assert_select "a[href=?]", root_path, count: 2
    assert_select "a[href=?]", help_path
    assert_select "a[href=?]", about_path
    assert_select "a[href=?]", contact_path
  end
end


Bu değişikliklerden sonra test çalıştırınca bir hata aldım ama testle alakalı değil 

$ rails rails t
# Running:

Error:
SiteLayoutTest#test_layout_links:
NoMethodError: assert_template has been extracted to a gem.
To continue using it, add `gem "rails-controller-testing"`
to your Gemfile.

assert_template metodu başka bir gem içine taşınmış ve bunu da Gemfile içine dahil edip bundle install yapmamız gerekiyormuş. Ben Gemfile içine bir değişiklik yapacaksam hemen uygulama klasörümün bir yedeğini alıyorum, size de tavsiye ederim. 

Haydi Gemfile içine mesajda geçen gem'i ekleyelim.

Gemfile

source "https://rubygems.org"

# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
gem "rails", "7.2.2"

gem "rails-controller-testing"
gem "bootstrap-sass", "3.4.1"
gem "sassc-rails", "2.1.2"
.....

ve bundle çalıştıralım

$ bundle

Gem yüklendikten sonra testimizi tekrar çalıştıralım.

$ rails t

Failure:
Expected exactly 2 elements matching "a[href=\"/\"]", found 1.

Ana Sayfa'da 2 tane root_path olması lazımdı biri logoya tıklayınca diğeri de navbar'ın üzerindeki Ana Sayfa yazısı, ama bir tane bulmuş testimiz. Eksiği _header.html.erb kısmi görselinde buldum. 

<header class="navbar navbar-fixed-top navbar-inverse">
  <div class="container">
    <%= link_to "Yeni App", "#", id: "logo" %>
    <nav>
      <ul class="nav navbar-nav navbar-right">
        <li><%= link_to "Ana Sayfa", root_path %></li>
        <li><%= link_to "Yardım", help_path %></li>
        <li><%= link_to "Giriş Yap", "#" %></li>
      </ul>
    </nav>
  </div>
</header>

Logodaki linki unutmuşum,

    <%= link_to "Yeni App", root_path, id: "logo" %>

olması gerekiyordu, düzeltip test çalıştırdım, sonuç başarılı oldu. Bu test kodundaki satırlara bir bakalım.

    assert_template "static_pages/home"

İsteği yapılan sayfa için "static_pages/home" görsel dosyasının yani home.html.erb dosyasının yayınlanması beklentisini ifade ediyor. 

    assert_select "a[href=?]", root_path, count: 2

<a> elemanları içinde <a href="/"> özelliğine sahip olan elemanlar bekliyor. count: 2 ile de bu beklentiye uyan 2 tane eleman olması beklendiği belirtiliyor. Bu assert_select metodunun birkaç örnek kullanımını görelim.

Kod                                           Eşleşen HTML örnek
----------------------------------------------------------------------------
assert_select "div"                           <div>foobar</div>
assert_select "div", "foobar"                 <div>foobar</div>
assert_select "div.nav"                       <div class="nav">foobar</div>
assert_select "div#profile"                   <div id="profile">foobar</div>
assert_select "div[name=yo]"                  <div name="yo">hey</div>
assert_select "a[href=?]", "/", count: 1      <a href="/">foo</a>
assert_select "a[href=?]", "/", text: "foo"   <a href="/">foo</a>


Sadece entegrasyon testlerinin çalışması için

$ rails test:integration
Running 1 tests in a single process (parallelization threshold is 50)
1 runs, 5 assertions, 0 failures, 0 errors, 0 skips




Kullanıcı kayıt işlemine giriş

Kullanıcılara bir giriş olsun diye bu bölümde bir kayıt olma sayfası linki ekleyeceğiz. Tamamlanması daha zaman alacak. Yeni bir kontrolörle başlayacağız, ilerleyen bölümlerde User modeli ve kayıt işlemlerini yapacağız. 


Users kontrolörü

Bundan önce Static Pages kontrolörümüzü oluşturduk Sıra ikinci kontrolörümüz Users kontrolörüne geldi. Daha önce olduğu gibi generate kullanacağız, ama sadece temel olarak yeni kayıt eylemi olacak şakilde. Rails REST ilkelerine uygun olarak yeni kullanıcı kayıt sayfasını new eylemine bağlayacağız. 

$ rails generate controller Users new
      create  app/controllers/users_controller.rb
       route  get "users/new"
      invoke  erb
      create    app/views/users
      create    app/views/users/new.html.erb
      invoke  test_unit
      create    test/controllers/users_controller_test.rb
      invoke  helper
      create    app/helpers/users_helper.rb
      invoke    test_unit


Bu komutla Users kontrolörü ve eylemleri dosyası, new eylemi için görsel dosyası ve kontrolör için bir test dosyası oluşturulur.

app/controllers/users_controller.rb

class UsersController < ApplicationController
  def new
  end
end


app/views/users/new.html.erb

<h1>Users#new</h1>
<p>Find me in app/views/users/new.html.erb</p>


test/controllers/users_controller_test.rb

require "test_helper"

class UsersControllerTest < ActionDispatch::IntegrationTest
  test "should get new" do
    get users_new_url
    assert_response :success
  end
end


Bu noktada test çalıştırırsak sorunsuz geçmelidir.

$ rails t
# Running:
......
6 runs, 14 assertions, 0 failures, 0 errors, 0 skips




Signup URL değeri

Yönlendirme dosyamızda 

  get "users/new"

satırı eklenmiş olmalıdır. Bunu "/signup" bağlantısını alacak ve isimlendirilmiş yönlendirme olacak şekilde, diğerleri gibi düzenleyelim.

config/routes.rb

Rails.application.routes.draw do
  root "static_pages#home"
  get "/help", to: "static_pages#help"
  get "/about", to: "static_pages#about"
  get "/contact", to: "static_pages#contact"
  get "/signup", to: "users#new"
....


Tabii ki test çakılacak hemen düzenleyelim

test/controllers/users_controller_test.rb

require "test_helper"

class UsersControllerTest < ActionDispatch::IntegrationTest
  test "should get new" do
    get signup_path
    assert_response :success
  end
end


Şimdi Ana Sayfa'daki "Hemen Katılın!" yazılı butonun linkini isimlendirilmiş yönlendirme kullanır şekle getirelim.

app/views/static_pages/home.html.erb

<div class="center jumbotron">

  <h1>Yeni Uygulama</h1>
  <h2>Bu uygulama ile Rails
    <a href="https://ujk-ujk.blogspot.com/2025/03/rails-7-denemeler-2.html#Statik%20Sayfalar">
    Statik Sayfaları</a> öğreniyoruz
  </h2>
  <%= link_to "Hemen Katılın!", signup_path,
          class: "btn btn-lg btn-primary" %>
</div>

<%= link_to image_tag("rails.svg", alt: "Rails logo", width: 200),
      "https://rubyonrails.org/" %>


Son olarak Kayıt Ol sayfasına kaba bir görünüm ekleyelim.

app/views/users/new.html.erb

<% provide :title, "Kayıt Ol" %>
<h1>Kayıt Olun</h1>
<p>Burası yeni kullanıcıların kayıt olma sayfası olacak</p>


Artık Ana Sayfada Kayıt Olun butonu tıklanınca bu geçici görselimiz açılacaktır. 


Bu bölümde öğrendiklerimiz

  • Html5 kulanarak header, footer, logo ve gövde içeriği olan sayfalar yapabiliriz.
  • Rails kısmi görselleri karmaşık görsel dosya kodlarını güzelleştirmek için çok faydalıdır.
  • CSS kuralları yazarak görsellerimizin stillerini değiştirebiliriz.
  • Bootstrap kütüphanesi kullanarak kolayca güzel görünümlü web sayfaları yapabiliriz.
  • Sass ve asset pipeline sayesinde tasarım esnasında ve üretim modunda ayrı CSS dosyalama teknikleri ile uğraşmadan performanslı uygulama geliştirebiliriz.
  • Rails ile kemdi yönlendirmelerimizi yapabilir, isimlendirilmiş yönlendirmeler kullanabiliriz.
  • Entegrasyon testleri ile tarayıcıda sayfadan sayfaya geçiş tıklamaları test edilebilir.




Kullanıcıları Düzenlemek

En son yeni kullanıcılar eklemek için bir sayfa temelini oluşturmuştuk. Bundan sonra bu Kayıt Olun sayfası üzerinden öğrenmeye devam edeceğiz. Bu bölümde ilk adımımız data modelimiz olan User modelini tanımlayacağız ve oraya veri atmak için düzeneği kuracağız. Sonraki bölümlerde kullanıcının kayıt olup kullanıcı sayfasını hazırlamasını gerçekleştireceğiz. 

Kendi yetkilendirme sisteminizi tasarlamak

Genelde tüm web uygulamalarında bir çeşit yetkilendirme sistemi olur. Sonuç olarak birçok web framework bu işleri görecek kütüphanelere sahiptir, Rails de öyledir. En yaygın kullanılan Devise gem'dir ve güçlü uygulamalar yapmakta kullanılır. Ancak öğrenmek amacımız olduğu için ve bu tip kütüphanelerin veri yapılarını anlamak ustalar için bile zor olduğu için, kendimize göre bir yol izleyeceğiz. 

Ancak tüm öğrendikleriniz sonunda hala 3. parti birilerinin geliştirdiği sistemi kullanmaya karar verirseniz en iyi seçim Devise olacaktır.   




User modeli

Hedefimiz yeni kullanıcıların kaydı olduğuna göre, kullanıcı bilgilerini saklayacak bir yere ihtiyacımız var. Kayıt sayfasına bir taslak hazırlarken toplayacağımız verileri de bir düşünelim.


Rails'de bir veri kaydı yapısını tarif ederken Model denir (MVC'deki M). Rails kalıcı verileri bir veritabanında saklamak için Active Record kütüphanesini kullanır. Active Record sınıfı verilerin bir bağıl veri tabanında saklanması , sorgulanması, değiştirilmesi, silinmesi vs SQL sorgulamaları yapmak için birçok metodlara sahiptir. Bundan başka Rails migration kodları sayesinde SQL data definition language (DLL) bilmeden saf Ruby kodları ile veri tabanında tanımlamaları kolayca yapabilirsiniz. Bu sayede geliştirme yaparken SQLite veri tabanı üretim modunda PostgreSQL veri tabanı kullanırken ayrı ayrı kodlar yazmanız gerekmez. 



Veri tabanı migrasyonları

Daha önce bir denemede örnek bir User sınıfını name ve email özellikleri ile tanımlamıştık. 

class User
  attr_accessor :name, :email
  def initialize(attributes = {})
    @name = attributes[:name]
    @email = attributes[:email]
  end
  def formatted_email
    "#{@name} <#{@email}>"
  end
end


Rails konsolda bu sınıfı kullanarak ürettiğimiz User nesneleri konsolu kapatınca yok oluyor (çöp oluyor) tabii ki. Bu bölümde amacımız kullanıcılar için bir model oluşturarak verilerin öyle kolayca yok olmasını önlemek. 

Daha önce tanımladığımız User sınıfı gibi Rails modelimizi tanımlarken de name ve email öznitelikleri ile başlayacağız, ama bir fark var, kullanıcı adı (name) kayıtlarda sadece bir tane aynısından olacak. Şifre için ilaveyi daha sonra yapacağız. 

Rails'de kullanıcıların modellemesini yaparken User sınıfı tanımında kullandığımız gibi attr_accessor metodu ile öznitelikleri belirtmiyoruz, bunun yerine modelimiz veri tabanındaki bir tabloya işaret ederken öznitelikler de o tablonun sütun başlıklarını gösterir. Örneğin isimleri ve email adresleri olan kullanıcıları üretmek için users tablosunu name ve email sütunları ile oluşturacağız. Bu tablodaki her satır bir kullanıcı kaydına karşı gelecektir. Hayalimizde şöyle kayıtları olan bir veri tablosu var.


Bu tablo için düşündüğümüz tablo yapısı ise


Rails modeli olarak bu tabloyu yaparsak elde edeceğimiz tablo ise


Daha önce kontrolör eklemek için 

$ rails generate controller Users new

kullandığımız gibi generate model ile de bir model ekleyebiliriz. 

$ rails g model User name:string email:string
      invoke  active_record
      create    db/migrate/20250404135314_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml

Burada dikkat edilmesi gereken nokta kullanıcılar için tasarlanan model için tek bir kaydı ifade eden User teriminin kullanılması (Users değil). Kontrolör adı Users fakat model adı User olmalı. Argümanlarda name:string  ve email:string vererek tablomuzda string değer olan name ve email sütunları olmasını istediğimiz ifade ediyoruz. 

Terminalde girdiğimiz generate komutunun ürettiği dosyalardan biri de db/migrate/ klasörü içinde ürettiği ..._create_users.rb migrasyon dosyası. Bu dosyayı kullanarak veri tabanında gereken tablo ilavemizi yaparız. Şimdi içine bir bakalım.

db/migrate/[timestamp]_create_users.rb

class CreateUsers < ActiveRecord::Migration[7.2]
  def change
    create_table :users do |t|
      t.string :name
      t.string :email

      t.timestamps
    end
  end
end


Migrasyon dosyasının adı üretildiği zamanı ifade eden bir değerle başlıyor. Mesela bendeki adı 20250404135314_create_users.rb . Yıl-ay-gün-saat-dakika-saniye birleşimi bu notasyonu ben tüm projelerimde yedek alırken .zip dosyalarının isimlerine eklerim ki geriye dönmem gerekirse istediğim tarihi bulabileyim. Rails kullanırken edinilen bu mantıklı isimlendirmeyi ekip arkadaşlarıma zorunlu yaptırıyorum. 

Migrasyon kodunda tanımlanan change metodu Rails'e veri tabanında bir değişim yapılacağını bildirir. Bu migrasyonda change metodu içinde create_table ile kullanıcılar için users adında yeni bir tablo ekleniyor. create_table metodu bir blok alıyor ve blok değişkeni olarak t değişkeni eklenecek olan users tablosunu işaret ediyor. Metod t değişkenini tabloya name ve email sütunlarını eklemek için kullanılıyor, her ikisi de string tipinde veriler içerecek. Buradaki tablo adı çoğul (users) ve modeldeki isim tekil (user),  bu Rails'in bir geleneği, bir  model tek bir kullanıcıyı ifade ederken tablomuzda birçok kullanıcının kaydı olacak. Kod bloğundaki son satırda t.timestamps yazıyor. bu özel metod created_at ve updated_at adında başlığı olan iki sütunu tabloya ekler. Bu iki sütunda sırasıyla kaydın eklendiği zaman ve en son değiştirildiği zaman bilgileri otomatik olarak Rails tarafından doldurulacak. 

Şimdi bu migrasyonu (yani birleştirmeyi) gerçekleştirip veri tabanımızın kullanıcılar için tablo sahibi olması için terminalde,

$ rails db:migrate
== 20250404135314 CreateUsers: migrating ==================
-- create_table(:users)
   -> 0.0078s
== 20250404135314 CreateUsers: migrated (0.0083s) =========

Dikkat edin db:migrate, benim hep düştüğüm hata gibi db/migrate değil. Eğer db:migrate komutunu ilk defa çalıştırırsak (ki Yeni App içinde ilk oldu) storage/development.sqlite3 adında bir veri tabanı dosyası oluşturur. Bu bir SQLite veri tabanıdır. Bende DB Browser for SQLite diye bir portatif uygulama var, bununla açınca veri tabanında users tablosu yapılanması


ve tabii ki şu anda içinde bir veri kaydı bulunmuyor.


Burada migrasyon dosyasında bile olmayan id sütunu Rails tarafından eklenir ve tüm tablolarda otomatik olarak eklenerek aynı değerlere sahip ama başka satırlarda girilmiş kayıtların birbirinden otomatik ayrılmasını sağlıyor. Bu sütuna girilen değer her kayıt girildikçe otomatik artar ve geri kalan değerler aynı olsa bile id değerleri hep farklı olur. Girilebilecek sayının üst limitine gelince ne olur? onu şimdilik bilmiyorum. 



Model dosyası

rails g model komutu ile oluşturduğumuz User modeli dosyasına bir bakalım.

app/models/user.rb

class User < ApplicationRecord
end

Burada User modelimizin sınıfının Rails'in ApplicationRecord sınıfından türetildiğini görüyoruz. Aslında o da ActiveRecord::Base sınıfından türetilmiştir. ActiveRecord::Base sınıfı içinde neler olduğunu anlamaya bazı örneklerle başlayalım.



User nesnelerini üretmek

Denemlerimizi Rails konsolda yapacağız, ancak şimdilik veri tabanına yeni kayıtlar eklememek için konsolu sandbox modunda başlatacağız. 

$ rails console --sandbox
Loading development environment in sandbox (Rails 7.2.2)
Any modifications you make will be rolled back on exit
>>


Bilgilendirme mesajından anlaşıldığı üzere yapılan tüm değişiklikler çıkışta geri alınacaktır. Daha önce konsolda User.new metodu kullanarak kullanıcı eklemesini görmüştük, ama bir sınıf tanımlaması kullanarak. Model kullanarak benzerini yaparken sınıf dosyasını yüklememize gerek yok, Rails otomatik olarak uygulamada tanımlı sınıf dosyalarını konsola getirecektir. Bu durumda direk olarak yeni User nesnesi üretebiliriz.  

>>  User.new
  TRANSACTION (0.0ms)  begin transaction
  TRANSACTION (0.3ms)  SAVEPOINT active_record_1
  TRANSACTION (0.1ms)  SAVEPOINT active_record_2
=> #<User:0x00007fa793cfe170 id: nil, name: nil,
     email: nil, created_at: nil, updated_at: nil>


Konsolda bir User nesnesinin nasıl gösterildiği, komutumuza dönen cevapta görülüyor. Argüman vermeden User.new metodu çağırdığımızda tüm öznitelikleri nil olan bir User nesnesi üretiliyor. Daha önce kendi sınıf tanımımızla User nesnesi üretirken argümanda bir hash değer vererek öznitelik değerlerini belirtmiştik. Model kullanırken de aynı davranılır. 

>> user = User.new(name: "Ümit Kayacık", email: "umit@example.com")
=>
#<User:0x00007fa791cff8d8
...
>> user
=>
#<User:0x00007fa791cff8d8
 id: nil,
 name: "Ümit Kayacık",
 email: "[FILTERED]",
 created_at: nil,
 updated_at: nil>


email bilgisine gösterimde [FILTERED] gelmesi ilginç. config/initializers/filter_parameter_logging.rb içinde :email değeri de filtrelenmişler içinde olduğu için log kayıtlarında konulmuyor. 

>> user.email
=> "umit@example.com"

Ama sorarsak konsolda kayıtlı değeri görürüz. 

User nesnesinin geçerliliği konusunda çalışacağız ama şimdilik kaydın geçerliliğini sorgulamak için valid? metodu kullanırız, onu bilelim.

>> user.valid?
=> true


Şu ana kadar user nesnesini sadece hafızada tanımlamış olduk, veri tabanına kaydetmek için bir eylem yapmadık. Bu amaçla user nesnesinde save metodu çağrılır. 

>> user.save
  TRANSACTION (0.1ms)  begin transaction
  TRANSACTION (0.1ms)  SAVEPOINT active_record_1
  User Create (13.0ms)  INSERT INTO "users"
    ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?)
    RETURNING "id"  [["name", "Ümit Kayacık"], ["email", "[FILTERED]"],
    ["created_at", "2025-04-14 09:33:35.266334"],
    ["updated_at", "2025-04-14 09:33:35.266334"]]
  TRANSACTION (0.1ms)  RELEASE SAVEPOINT active_record_1
=> true


save metodu başarılı olursa true , olamazsa false döner (Şu anda tüm kaydetme girişimleri başarılı olur, çünkü herhangi bir veri doğrulaması yapılmıyor, ama yakın zamanda yapacağız). Girdiğimiz komut sonucu konsola gelen mesajda veri tabanında yapılan SQL işleminin kodu da görülüyor, Rails bu kodları bizim için üretir ve çalıştırır. 

user nesnesine şimdi bakarsak daha önce değerleri nil olan idcreated_at ve updated_at sütunlarındaki değerlerin save komutuyla beraber otomatik doldurulduğunu görürüz. 

>> user
=>
#<User:0x00007fa791cff8d8
 id: 1,
 name: "Ümit Kayacık",
 email: "[FILTERED]",
 created_at: "2025-04-14 09:33:35.266334000 +0000",
 updated_at: "2025-04-14 09:33:35.266334000 +0000"> 


id değeri ilk kayıt olduğu için otomatikman 1 olurken created_at ve updated_at değerleri ise kaydı oluşturduğumuz zamanı gösteriyor. 

Ürettiğimiz User nesnesinin özniteliklerine noktalı notasyonla ulaşabiliriz.

>> user.name
=> "Ümit Kayacık"
>> user.email
=> "umit@example.com"
>> user.created_at
=> Mon, 14 Apr 2025 09:33:35.266334000 UTC +00:00


Daha ileride göreceğiz genel olarak bir kaydı veri tabanına eklerken yukarıda yaptığımız gibi iki adımda yapmak tavsiye edilir. Ama istersek create metodu kullanarak iki adımı bir arada gerçekleştirebiliriz.

>> User.create(name: "Bir Başka", email: "bir_baska@example.com")
=> #<User:0x00007fa791cfbb98
 id: 2,
 name: "Bir Başka",
 email: "[FILTERED]",
 created_at: "2025-04-14 09:55:11.727141000 +0000",
 updated_at: "2025-04-14 09:55:11.727141000 +0000">
 
>> foo = User.create(name: "Foo Bar", email: "foo@example.com")
=> #<User:0x00007fa791cf3858
 id: 3,
 name: "Foo Bar",
 email: "[FILTERED]",
 created_at: "2025-04-14 09:55:39.702164000 +0000",
 updated_at: "2025-04-14 09:55:39.702164000 +0000">


Kaydı silmek için destroy metodu kullanırız. 

>> foo.destroy
  TRANSACTION (0.1ms)  SAVEPOINT active_record_22
  TRANSACTION (0.1ms)  SAVEPOINT active_record_23
  User Destroy (1.5ms)  DELETE FROM "users" WHERE "users"."id" = ?  [["id", 3]]
  TRANSACTION (0.1ms)  RELEASE SAVEPOINT active_record_23
=>
#<User:0x00007fa791b2ba48
 id: 3,
 name: "Foo Bar",
 email: "[FILTERED]",
 created_at: "2025-04-14 09:55:39.702164000 +0000",
 updated_at: "2025-04-14 09:55:39.702164000 +0000">


Veri tabanından bu kayıt silinmiş olmasına rağmen hala hafızada duruyor.

>> User.all
....
=> [#<User:0x00007fa791b2f008
  id: 1,
....
   #<User:0x00007fa791b2eec8
  id: 2,
....]

>> foo
=> #<User:0x00007fa791b2ba48
 id: 3,
....




User nesnelerini bulmak

Active Record, User tablosu kayıtlarına erişmek için çeşitli imkanlar sunar. Bunlar User sınıfının sınıf metodlarıdır.

>> User.find(1)
=> #<User:0x00007fa791b25148
 id: 1,
 name: "Ümit Kayacık",
 email: "[FILTERED]",
 created_at: "2025-04-14 10:17:38.311202000 +0000",
 updated_at: "2025-04-14 10:17:38.311202000 +0000">


find metodu parametresinde verilen id değerine ait kaydı veri tabanı tablosundan bulup getirir. 

Bu arada sandbox modda olduğumuz için bir süre geçince kayıtlar veri tabanından otomatik silinecektir, öyle olursa şaşırmayın. 

Sildiğimiz 3 numaralı kayda bakalım

>> User.find(3)
/usr/share/rvm/gems/ruby-3.2.7/gems/irb-1.15.1/lib/irb.rb:406:in `full_message':
Couldn't find User with 'id'=3 (ActiveRecord::RecordNotFound)


Belli bir öznitelik değerine sahip kaydı bulmak için find_by metodu kullanırız. 

>> User.find_by(email: "umit@example.com")
=> #<User:0x00007fa791cf3c18
 id: 1,
 name: "Ümit Kayacık",
 email: "[FILTERED]",
 created_at: "2025-04-14 10:31:21.130531000 +0000",
 updated_at: "2025-04-14 10:31:21.130531000 +0000">


Tablodaki ilk kayıt için first metodu kullanırız. 

>> User.first
=> #<User:0x00007fa791b2be08
 id: 1,
 name: "Ümit Kayacık",
 email: "[FILTERED]",
 created_at: "2025-04-14 10:31:21.130531000 +0000",
 updated_at: "2025-04-14 10:31:21.130531000 +0000">


Tüm kayıtları bir array olarak all metodu ile okuruz.

>> User.all
=> [#<User:0x00007fa791b2f788
  id: 1,
  name: "Ümit Kayacık",
  email: "[FILTERED]",
  created_at: "2025-04-14 10:31:21.130531000 +0000",
  updated_at: "2025-04-14 10:31:21.130531000 +0000">,
 #<User:0x00007fa791b2f648
  id: 2,
  name: "Bir Başka",
  email: "[FILTERED]",
  created_at: "2025-04-14 10:31:29.796687000 +0000",
  updated_at: "2025-04-14 10:31:29.796687000 +0000">]




User kayıtlarını değiştirmek

Nesneyi değiştirmek kolay , ancak veri tabanındaki kaydı değiştirmek için başka davranılır. 

>> user.email = "ukayacik@example.com"
=> "ukayacik@example.com"
>> user.save
=> true
>> User.find(1).email
=> "ukayacik@example.com"


save kullanmazsak veri tabanında kayıt değişmez bunu test için reload ile kaydı veri tabanından geri çağırabiliriz. 

>> user.email = "foo@bar.com"
=> "foo@bar.com"
>> user.email
=> "foo@bar.com"
>> user.reload.email
=> "ukayacik@example.com"


Direk olarak update nesnenin metodunu kullanarak da kayıt değiştirebiliriz, ve bunu updated_at özellik değerinden değiştiğini görebiliriz.

>> user.updated_at
=> Mon, 14 Apr 2025 10:40:45.208494000 UTC +00:00
>> user.update name: "Hasan Bey", email: "bilinmiyor@foo.org"
=> true
>> user.email
=> "bilinmiyor@foo.org"
>> user.updated_at
=> Mon, 14 Apr 2025 10:49:09.887388000 UTC +00:00


update_attribute metodu ise bir tek özelliği değiştirmek için kullanılır. 

>> user.updated_at
=> Mon, 14 Apr 2025 10:49:09.887388000 UTC +00:00
>> user.update_attribute :name, "Oğuzhan Uzunkavaklaraltındayataruyumazoğlu"
=> true
>> user.name
=> "Oğuzhan Uzunkavaklaraltındayataruyumazoğlu"
>> user.updated_at
=> Mon, 14 Apr 2025 10:54:07.500935000 UTC +00:00

Rails konsolda exit yazıp çıkalım.




User kayıt doğrulamaları

Ürettiğimi User modelimiz name ve email öznitelikleri ile çalışmakta, ancak çok sıradan şekilde. Herhangi bir string değeri bu özniteliklere girsek yeterli oluyor. Ama bundan çok daha farklı olmalı örneğin name değeri boş bir string olmamalı ve email değeri bir e-mail adresi formatına sahip olmalı. Dahası ileride email değerini sisteme giriş yapmak için kullanacağımızdan tabloda aynı email değerine sahip tek bir kayıt olmalı. 

Kısacası name ve email değerleri öyle sıradan bir string olamaz ve bunları sisteme kaydetmeden önce kontrol etmeliyiz. Active Record bize validations adı verilen işlemlerle bu kontrolleri yapma imkanı sunar. En genel kontroller, presence, length, format ve uniqueness


Geçerlilik testi

Daha önce de TDD (test driven development) tekniği kullanmıştık. TDD'nin en anlamlı olduğu yerlerden biri bu geçerlilik testleridir. Yazdığımız geçerlilik kurallarının çalıştığını görmenin en iyi yolu geçerli olmayan değerlerle kayıt yapmayı deneyen testler yazmaktır. 

Öncelikle geçerli bir User kaydını test eden bir rutinle başlayalım. Şu anda test dosyamızın içinde bir test rutini yok.

test/models/user_test.rb

require "test_helper"

class UserTest < ActiveSupport::TestCase
  # test "the truth" do
  #   assert true
  # end
end


Öncelikle setup metodu içinde geçerli bir @user nesnesi tanımlayacağız, sonra test bloğunda bu kaydın geçerliliğini test edip testten geçmesini bekleyeceğiz. 

require "test_helper"

class UserTest < ActiveSupport::TestCase
  def setup
    @user = User.new(name: "Örnek User", email:
   "user@example.com")  
  end

  test "geçerli olmalı" do
    assert @user.valid?
  end
end  


Burada assert metodu beklentimizi bildiriyor, yani @user.valid? işlemi true değer dönerse bu test başarılıdır. Test'i çalıştıralım.

$ rails test:models
Running 1 tests in a single process
....
Finished in 0.378634s, 2.6411 runs/s, 2.6411 assertions/s.
1 runs, 1 assertions, 0 failures, 0 errors, 0 skips


1 test çalışmış 1 beklenti varmış, hepsi geçmiş, hata olmamış. Sadece modelleri test etmek için rails test:models komutu kullandık.



Presence geçerlilik kuralı

En temel geçerlilik kuralı presence , özelliğe bir değer girildiğini kontrol eder. Örneğin bu bölümde name ve email değerlerinin verildiğini kontrol edeceğiz. Bu amaçla geçmeyen bir test yazacağız Sonra da değerin girilmesini zorunlu hale getirmek için modele ilave yapacağız. 

Şu anda yeni kayıt açarken name değeri vermemizi ya da boş bir string vermemizi engelleyen bir şey yok. Yazacağımız ikinci testte boşluklardan oluşan bir değerin geçmemesini beklenti olarak bildireceğiz. 

test/models/user_test.rb

require "test_helper"

class UserTest < ActiveSupport::TestCase
  def setup
    @user = User.new(name: "Örnek User", email:
   "user@example.com")  
  end

  test "geçerli olmalı" do
    assert @user.valid?
  end
  test "name değeri verilmeli" do
    @user.name = " "
    assert_not @user.valid?
  end
end  


name değeri boşluk olarak verilince testten geçmesin diye assert_not ile beklentimizi ters çeviriyoruz. Şimdi test edelim.

$ rails test:models
Running 2 tests in a single process
...
Failure:
UserTest#test_name_değeri_verilmeli [test/models/user_test.rb:15]:
Expected true to be nil or false
...
Finished in 0.068518s, 29.1895 runs/s, 29.1895 assertions/s.
2 runs, 2 assertions, 1 failures, 0 errors, 0 skips


UserTest#test_name_değeri_verilmeli ifadesi bize yazdığımız test kodunun neresinde geçmediğini belirtiyor, bizim test için verdiğimiz konu başlığını aralara alt çizgi ekleyerek yazmış, demek metod gibi kullanıyor. 

Şimdi modelimizin kodunda validates metodu kullanarak bir geçerlilik kuralı oluşturalım.

app/models/user.rb

class User < ApplicationRecord
  validates :name, presence: true
end


validates metodu birinci parametresi geçerlilik kuralı yazılacak öznitelik adı, ikincisi ise istenen geçerlilik kuralı. Burada presence: true ile name özniteliğine değer girilmesi gerektiği (presence=mevcut) bildiriliyor. Şimdi konsolu açıp name değeri olmayan bir kayıt oluşturup geçerliliğini kontrol edelim, bakalım ne olacak.

$ rails console --sandbox
>> user = User.new(name: " ", email: "umit@example.com")
=> #<User:0x00007f779752a650 id: nil, name: " ", ...>
>> user.valid?
=> false


Gördüğümüz gibi geçerli bir kayıt değil. valid? metodu ile user nesnesinin geçerliliğini kontrol edince false değer dönerek bize geçerli olmadığını bildiriyor. Uygulamamız çalışırken bize Rails tarafından verilecek mesajı görmek istersek. 

>> user.errors.full_messages
=> ["Name can't be blank"]


İsim boş olamaz diyor. Aslında mesaj bize Rails'in bir özelliğin mevcut olmasını blank? metodu ile kontrol ettiğini gösteriyor. Geçerli bir kayıt olmadığı için veri tabanına kayıt etmeye kalkarsak başarısız olacaktır.

>> user.save
=> false


Konsoldan çıkıp bir de test çalıştıralım.

$ rails test:models
Running 2 tests in a single process
...
Finished in 0.066230s, 30.1978 runs/s, 30.1978 assertions/s.
2 runs, 2 assertions, 0 failures, 0 errors, 0 skips


Başarılı bir test oldu demek ki doğru yere müdahale ettik. Şimdi aynı adımları email özelliği için de uygulayalım.

test/models/user_test.rb

require "test_helper"

class UserTest < ActiveSupport::TestCase
....

  test "email değeri verilmeli" do
    @user.email = ""
    assert_not @user.valid?
  end
end  


ve model

app/models/user.rb

class User < ApplicationRecord
  validates :name, presence: true
  validates :email, presence: true
end


Test edersek başarılı sonuç almalıyız. 



Length geçerlilik kuralı

İsim ve e-mailllerin karakter sayıları iki bakımdan sınırlanması gerekiyor. Birincisi ismi sayfamızda yayınlayacağız ve görsel açıdan saçma olmasın diye örneğin maksimum 50 karakterle name öznitelik değerini sınırlamalıyız. email değerini sayfada göstermeyecek olsak bile birçok veri tabanı için maksimum string uzunluğu olan 255 karakterden uzun olmamasını sağlamalıyız. 

Öncelikle bu iki sınırı aşan değerlerin geçmemesi gerektiğini test kodlarımıza ekleyelim.

test/models/user_test.rb

require "test_helper"

class UserTest < ActiveSupport::TestCase
  def setup
    @user = User.new(name: "Örnek User", email:
   "user@example.com")  
  end
....

  test "name çok uzun olmamalı" do
    @user.name = "a" * 51
    assert_not @user.valid?
    end

  test "email çok uzun olmamalı" do
    @user.email = "a" * 244 + "@example.com"
    assert_not @user.valid?
  end
end  


Burada string değerde yapılan çarpma işlemini bir açıklayalım. Konsolda bunları deneyip görebiliriz. 

>> "a" * 51
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
>> ("a" * 51).length
=> 51
>> ("a" * 244 + "@example.com").length
=> 256


Burada gördüğümüz length metodu verilen string değerin karakter sayısını bize döner. 

Tabii ki test yaparsak başarısız olacaktır, çünkü henüz bu konularda bir geçerlilik kuralı tanımlamadık.

$ rails test:models
Running 5 tests in a single process (parallelization threshold is 50)
...
Failure:
UserTest#test_name_çok_uzun_olmamalı
Expected true to be nil or false
...
Failure:
UserTest#test_email_çok_uzun_olmamalı
Expected true to be nil or false
...
Finished in 6.478268s, 0.7718 runs/s, 0.7718 assertions/s.
5 runs, 5 assertions, 2 failures, 0 errors, 0 skips


Şimdi modelimize bu iki uzunluk iin de geçerlilik kuralları ekleyelim.

app/models/user.rb

class User < ApplicationRecord
  validates :name, presence: true, length: { maximum: 50 }
  validates :email, presence: true, length: { maximum: 255 }
end


length (uzunluk-karakter sayısı) özelliğinin maximum alt özelliği, maksimum uzunluk geçerliliği için kurala ilave yapıyor

  validates :name, presence: true
  validates :name, length: { maximum: 50 }

Şeklinde ayrı kurallar da yazabilirdik, ama aynı özellik için olan kuralları tek satırda birleştirmek daha doğru. 

Şimdi test yaparsak başarılı olacaktır.



Format geçerlilik kuralı

Bizim name özelliği için geçerlilik kurallarımız basit, boş olmayacak ve 50 karakterden uzun olmayacak. Ancak email özelliği için daha fazlasına ihtiyacımız var. Bir email formatı içinde neler olmalıdır? En azından bir harf ile başlamalı, arkasından harfler ya da nokta ya da alt çizgiler olabilir, arkasından @ karakteri gelir, arkasından harfler ve noktalar gelir, en sonda yine bir harf olmalıdır. 

Bu karmaşık şablonu ifade etmek için tabii ki regexp kullanacağız. Bu regexp işleri her zaman kafamı karıştırır, yavaş ilerleyelim. Öncelikle birkaç tane geçerli adres bilgisi tanımlayıp bunların testten geçtiğini bir görelim. Birkaç değer ile test yapabilmek için önce veri kolleksiyonlarını konsolda birkaç deneme ile görelim.

>> %w[foo bar baz]
=> ["foo", "bar", "baz"]
>> adresler = %w[USER@foo.COM THE_US-ER@foo.bar.org first.last@foo.jp]
=> ["USER@foo.COM", "THE_US-ER@foo.bar.org", "first.last@foo.jp"]

?> adresler.each do |adres|
?>   puts adres
>> end

USER@foo.COM
THE_US-ER@foo.bar.org
first.last@foo.jp

%w[ ] notasyonun kullanılınca Ruby köşeli parantez içindeki kelimeleri boşluklardan ayırıp, her biri bir string olan elemanlardan oluşan bir array üretir. each metodu uygulandığı koleksiyonun her bir elemanı için kendisine verilen kod bloğunu çalıştırır, bloğa iterasyonu yapılan eleman |adres| şeklinde iki bar arasında değişken adı olarak verilir ve blok içinde bu değişken adı ile kullanılabilir. 

Öncelikle bu geçerli adreslerin kabul gördüğü bir test yazalım ki, daha sonra geçersizleri kabul etmeyecek olan rutinimizin , bunları kabul etmeye devam edeceğini garanti altına alalım. 

test/models/user_test.rb

require "test_helper"

class UserTest < ActiveSupport::TestCase
  def setup
    @user = User.new(name: "Örnek User", email:
   "user@example.com")  
  end
....
....
....
  test "geçerli email adresler kabul edilmelidir" do
    geçer_adresler = %w[user@example.com USER@foo.COM A_USER@foo.bar.org
    first.last@foo.jp alice+bob@baz.cn]
    geçer_adresler.each do |adres|
      @user.email = adres
      assert @user.valid?, "#{adres.inspect} geçerli olmalı"
    end
  end
end  


Beklentimize (assert satırı) bir argüman daha ekledik. Burada bir string enterpolasyonu var. adres.inspect bize adres değerinin Ruby'nin anladığı şekilde ifadesini verir, örneğin.

>> puts adresler[0].inspect
"USER@foo.COM"
>> puts adresler[0]
USER@foo.COM


Bu eklediğimiz ikinci argüman ile biz beklentimizi koleksiyondaki hangi değerin karşılamadığını konsola yazdırmasını sağlıyoruz. Bir test başarısızlığı olursa hangi adres için olduğu da yazılacak. 

Şimdi bir test bloğu da geçersiz olmasını istediğimiz email adresleri için yazalım. 

require "test_helper"

class UserTest < ActiveSupport::TestCase
  def setup
    @user = User.new(name: "Örnek User", email:
   "user@example.com")  
  end
....
....
....
  test "geçersiz email adresler red edilmelidir" do
    geçmez_adresler = %w[user@example,com user_at_foo.org
      user.name@example. foo@bar_baz.com foo@bar+baz.com]
    geçmez_adresler.each do |adres|
      @user.email = adres
      assert_not @user.valid?, "#{adres.inspect} red edilmeli"
    end
  end
end  


Şimdi test yaparsak başarısız olacaktır.

$ rails test:models
Running 7 tests in a single process
...
Failure:
UserTest#test_geçersiz_email_adresler_red_edilmelidir
[test/models/user_test.rb:47]:
"user@example,com" red edilmeli
...
7 runs, 11 assertions, 1 failures, 0 errors, 0 skips


Gördüğümüz gibi geçersiz olan ilk email adresi değeri ile birlikte mesaj veriyor. Koleksiyon değerlerinden bir tanesi bile testi geçemezse test başarısız demektir. 

Gelelim geçerlilik kuralına , bir formatta veri isteyen geçerlilik kuralımız şöyle olur.

  validates :email, format: { with: /<regular expression>/ }


Regexp (regular expression) stringlerin belirtilen formatla eşleşmesini görmek için kullanılan değer ifadesidir. Şimdi öyle bir regexp ifadesi yazacağız ki , geçerli adreslerle eşleşecek ama geçersiz adreslerle eşleşmeyecek. Tüm email adres standartlarını karşılayan regexp değerleri internette bulabiliriz, ama bunlar çok karmaşık olabiliyor. Biz şimdilik aklımıza gelenlerle bir ifade oluşturalım.

VALID_EMAIL_REGEX = /\A[\w+-.]+@[a-z\d\-.]+\.[a-z]+\z/i

Bu değerin ne anlama geldiğini rubular.com adresinde bakabiliriz. Kısaca sıralayalım

  • /  / arasında regexp ifadesi yazılır (stringlerdeki tırnaklar gibi bir sınırlayıcı)
  • /  /i eşleşme yapılırken büyük-küçük harf bakılmayacağını belirtir.
  • \A stringin başını yakalar, burada adrres öncesinde boşluk vs olmaması için bunu koyduk
  • \z stringin sonunu yakalar, bunu da adres sonunda boşluk vs olmaması için koyduk.
  • [  ] arasında o noktada olası karakterler verilir.
  • \w herhangi kelime karakteri demektir, yani harf, sayı ya da alt çizgi.
  • [\w+-.] herhangi kelime karakteri ya da + ya da - ya da . işareti demek
  • [\w+-.]+ üsttekinden yan yana bir sürü olabilir demek
  • @ bildiğimiz @ karakteri olacak
  • [a-z\d\-.]+ a'dan z'ye harfler ya da bir sayı ya da - ya da . işaretlerinden bir sürü yan yana olabilir demek. Yani @ sonrasında mesela + olamaz.
  • \.[a-z]+ en sonda bir nokta olacak ve arkasından gelen bir ya da bir sürü harf olacak demek


Şimdi modelimize bu format geçerlilik kuralını ekleyelim.

app/models/user.rb

class User < ApplicationRecord
  validates :name, presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+-.]+@[a-z\d\-.]+\.[a-z]+\z/i  
  validates :email, presence: true, length: { maximum: 255 },
      format: { with: VALID_EMAIL_REGEX }
end

Bu regexp ifadesi tam bir email geçerlilik paterni değil ama şimdilik idare eder. Örneğin  user@example..com gibi çift nokta yanyana bir email adresi kabul edilmemeli ama bizim verdiğimiz şablon için bu geçerli bir email adresidir. 



Birtek olma geçerlilik kuralı

Kullanıcılar giriş yaparlarken isim benzerliği olabilir diye email adreslerini esas kabul edeceğiz. Bu durumda kayıt yapan kişinin email adresinin aynısı Users tablosunda olmamamlıdır. Geçerlilik kurallarında :uniqueness seçeneği bu kontrolü yapar. Ama olay bu kadar basit değil, ilerledikçe göreceğiz. 

Öncelikle , daha önce kaydı veri tabanına atmadan önce kontrol yapılıyordu, ama şimdi önce bir kaydı veri tabanına saklayıp sonra aynı email adresiyle ikinci bir kayıt test etmeliyiz. Önce geçerli olmaması beklenen bir test yazalım. 

test/models/user_test.rb

require "test_helper"

class UserTest < ActiveSupport::TestCase
  def setup
    @user = User.new(name: "Örnek User", email:
   "user@example.com")  
  end

.....

  test "email adresi benzersiz olmalı" do
    duplicate_user = @user.dup
    @user.save
    assert_not duplicate_user.valid?
  end
end  


Burada @user.dup ile @user nesnesi ile aynı özelliklere sahip yeni bir nesne kopya olarak üretiliyor. @user nesnesindeki kaydı veri tabanına kaydettikten sonra duplicate_user nesnesi aynı email değerine sahip olduğu için artık geçersiz olmalıdır. Bu şekilde test edersek başarısız olacaktır.

Şimdi geçerlilik kuralımıza ilave yapalım. 

app/models/user.rb

class User < ApplicationRecord
  validates :name, presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+-.]+@[a-z\d\-.]+\.[a-z]+\z/i  
  validates :email, presence: true, length: { maximum: 255 },
      format: { with: VALID_EMAIL_REGEX },
      uniqueness: true
end


Daha bitmedi, email adresleri büyük-küçük harften bağımsızdır, yani foo@bar.com, FOO@BAR.COM ve Foo@bAr.CoM aynı email adresleridir. Test koduna şu satırı ekleyelim.

  test "email adresi benzersiz olmalı" do
    duplicate_user = @user.dup
    duplicate_user.email = @user.email.swapcase
    @user.save
    assert_not duplicate_user.valid?
  end


swapcase metodu verilen string'in harflerinin tek tek büyükse küçük , küçükse büyük yapar. Bu durumda kopya kayıttaki email adresi aynı ama harf büyüklükleri farklı olacaktır, yine de testten geçmemesi lazım. Ama şu halde test yaparsak başarısız olur.  Şimdi geçerlilik kuralımıza büyük-küçük bakılmaksızın benzersizlik istediğimizi belirtelim. 

app/models/user.rb

class User < ApplicationRecord
  validates :name, presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+-.]+@[a-z\d\-.]+\.[a-z]+\z/i  
  validates :email, presence: true, length: { maximum: 255 },
      format: { with: VALID_EMAIL_REGEX },
      uniqueness:  { case_sensitive: false }
end


Kafa karışmasın uniqueness özelliğine blok içinde alt özellikler verince önceki gibi true yazmamıza gerek yok. case_sensitive: false  ise büyük küçük harfe bakılmaksızın benzersiz değer olmalı kuralı.

Bu şekilde Rails'e birtek kayıt kuralı yazabiliriz. Bu kontrolün yetersiz olduğuna dair bazı şikayetler oluşmuş, Veri tabanına kayıt için sırada beklerken (yoğun trafikli sitelerde) çift kayıt olma işlemleri ile karşılaşılmış. Örneğin şu yazıda benzer problem anlatılıyor. 

Eğer trafiği yüksek bir siteniz olacaksa bu konuda bir araştırma yapmanızı tavsiye ederim. Genelde bu tip konuları çözerken veri tabanında kayıt yapılırken doğrulamalar yapılıyor, o da aslında sebebi tahmin edilmek zorunda kalınan kayıt etme hataları dönme ihtimali var. İşi veri tabanına bıraksak bile bazı veri tabanı motorlarının büyük-küçük harf kayıtlarda sorun yaşaması sebebiyle, en azından şu email adresini kaydetmeden önce küçük harfe çevirelim. Bu amaçla model dosyamızda kayıt öncesi çalışan bir callback fonksiyonu eklemeliyiz. 

app/models/user.rb

class User < ApplicationRecord
  before_save { self.email = email.downcase }
  validates :name, presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+-.]+@[a-z\d\-.]+\.[a-z]+\z/i  
  validates :email, presence: true, length: { maximum: 255 },
      format: { with: VALID_EMAIL_REGEX },
      uniqueness:  true
end


Kayıt etmeden önce küçük harfe çevireceğimiz için case_sensitive ifadesini kaldırdık. Test rutininde de büyük-küçük farkını denememize artık gerek yok.


test/models/user_test.rb

  test "email adresi benzersiz olmalı" do
    duplicate_user = @user.dup
    #duplicate_user.email = @user.email.swapcase
    @user.save
    assert_not duplicate_user.valid?
  end




Şifre eklemek

Kullanıcı adı ve email adresleri için geçerlilik kurallarımızı ekledik. Sırada kullanıcı için gerekli son öznitelik olan güvenli bir şifre eklenmesi var. Temelde kullanıcıdan bir şifre girmesini zorunlu kılacağız (ikinci bir doğrulama koopyası ile) ve şifreyi veri tabanına kaydederken bir hash olarak kaydedeceğiz. Burada hash dediğimiz bizim Ruby'deki hash tipi veri değil, veriyi geri döndürülemez bir hash değere dönüştürmek. 

Kullanıcıyı yetkilendirme yöntemi, girdiği şifreyi hash değere dönüştürmek ve veri tabanındaki hash değerle karşılaştırmak. Eğer eşleşirlerse girilen şifre geçerlidir ve kullanıcı yetkilendirilir. Hash değere dönüştürülmüş değerleri karşılaştırmak kullanıcının şifresinin direk olarak veri tabanında saklamadan kullanmamıza imkan sağlar. Yani veri tabanımız başkalarının eline geçse bile şifreler hala güvende olacaktır. 



Hash değere dönüştürülmüş şifre

Birçok güvenli şifre mekanizması model dosyasında Rails'in has_secure_password metodunun çağrılması ile gerçekleştirilir. 

class User < ApplicationRecord
  ....
  has_secure_password
end


Bu şekilde model dosyasına eklediğimizde bu metod şu ilave özellikleri kazandırır:

  • Güvenli şekilde hash değere dönüştürülmüş password_digest özniteliğini veri tabanına saklama
  •  Bir çift sanal öznitelik ekler - password ve password_confirmation - bunlar veri tabanına saklanmaz sadece nesne üretilirken kullanılan öznitelikler olarak modelde mevcuttur. Ayrıca bunların girilme zorunluluğu ve eşlenik olmaları zorunluluğu kuralları da eklenir.
  • authenticate metodu ile girilen şifre başarılı ise true değilse false değer alınır.


Bu sihirli metodun tek ihtiyacı modelimize ve veri tabanına password_digest özniteliğini eklememizdir. Bu amaçla rails generate komutunu kullanacağız. 

$ rails generate migration add_password_digest_to_users password_digest:string
      invoke  active_record
      create    db/migrate/20250422131814_add_password_digest_to_users.rb


Migrasyon dosyamıza bir bakalım.

class AddPasswordDigestToUsers < ActiveRecord::Migration[7.2]
  def change
    add_column :users, :password_digest, :string
  end
end


users tablosuna string olarak password_digest adında bir sütun ekler. Şimdi migrasyonları çalıştıralım.

$ rails db:migrate


has_secure_password şifreleri password_digest haline çevirmek için efsane bcrypt rutinini kullanır. Rails uygulamamıza bu fonksiyonu ekelemek için bcrypt gem'ini GemFile dosyamıza eklemeliyiz. 

GemFile

source "https://rubygems.org"

# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
gem "rails", "7.2.2"
gem "bcrypt", "3.1.18"
.....


Şimdi bundle ile ilave ettiğimiz gem'in uygulamamıza eklenmesini sağlayalım.

$ bundle
....
Fetching bcrypt 3.1.18
Installing bcrypt 3.1.18 with native extensions
...
Bundle complete! 20 Gemfile dependencies, 110 gems now installed.
...




Kullanıcı güvenli şifreye sahip mi?

User modelimize password_digest özniteliğini ekledik ve bcrypt gem'i de uygulamamıza dahil ettik. Şimdi modelimize eklemediysek has_secure_password satırını ekleyelim.

app/models/user.rb

class User < ApplicationRecord
  before_save { self.email = email.downcase }
  validates :name, presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+-.]+@[a-z\d\-.]+\.[a-z]+\z/i  
  validates :email, presence: true, length: { maximum: 255 },
      format: { with: VALID_EMAIL_REGEX },
      uniqueness:  true

  has_secure_password
end


Şimdi test yaparsak başarısız olacaktır.

$ rails test:models
...
Failure:
UserTest#test_geçerli_olmalı [test/models/user_test.rb:10]:
Expected false to be truthy.


Bunun sebebi has_secure_password metodu modelde password ve password_confirmation sanal özniteliklerini arıyor fakat bizim test setup kısmında verdiğimiz örnek @user nesnesinde bu öznitelik değerlerini girmedik tabii ki. 

  def setup
    @user = User.new(name: "Örnek User", email:
   "user@example.com")  
  end


Bu kısımda password ve password_confirmation özelliklerine değer girmeliyiz.

test/models/user_test.rb

require "test_helper"

class UserTest < ActiveSupport::TestCase
  def setup
    @user = User.new(name: "Örnek User", email:
      "user@example.com", password: "foobar",
      password_confirmation: "foobar")  
  end


Artık testten geçer. İsterseniz farklı şifre ve onay şifre girerek onların eşlenik olmasını da test edebilirsiniz, ikisinin aynı olmaması durumunda da hata verecektir. 




Minimum şifre standartları

Şifreler tahmin edilmesi güç olmalıdır. Bunu sağlamak için birçok geçerlilik testi yapmalıyız, ama olayı basit tutmak için şimdilik boş olmaması ve en az 6 karakter uzunlukta olmasını düşünelim. Bu amaçla test kodumuza iki test daha ekleyelim.

test/models/user_test.rb

require "test_helper"

class UserTest < ActiveSupport::TestCase
  def setup
...
...
  test "şifre mevcut olmalı (nonblank)" do
    @user.password = @user.password_confirmation = " " * 6
    assert_not @user.valid?
  end

  test "şifre minimum 6 karakter olmalı" do
    @user.password = @user.password_confirmation = "a" * 5
    assert_not @user.valid?
  end
end  


Şu anda geçerlik kuralı eklemediğimiz için test başarısız olacaktır. Model dosyamıza bu iki geçerlik kuralını tek satırda ekleyebiliriz.

app/models/user.rb 

class User < ApplicationRecord
  before_save { self.email = email.downcase }
  validates :name, presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+-.]+@[a-z\d\-.]+\.[a-z]+\z/i  
  validates :email, presence: true, length: { maximum: 255 },
      format: { with: VALID_EMAIL_REGEX },
      uniqueness:  true

  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }
end




Bir kullanıcı üretmek ve yetkilendirmek

User modelimizin temel tanımlaması bitti. Şimdi bir kullanıcı tanımlayıp , görsel sayfasını tasarlayacağız. Daha önce has_secure_password metodunu anlatırken uygulamamıza authenticate adında önemli bir metod eklediğinden bahsetmiştik. 

Kullanıcının kayıt yapması ve yetkilenmesi için uygulamaya ilaveleri sonraki bölümde yapacağız. Ancak bu bölümde konsolda bir kullanıcı ekleyeceğiz. Bu kullanıcının kayıtlı kalması için konsolu sandbox modunda değil normal olarak açacağız.

$ rails console
>> User.create(name: "Ümit Kayacık", email: "umit@example.com",
    password: "foobar", password_confirmation: "foobar")
  TRANSACTION (0.1ms)  begin transaction
  .....
=>
<User:0x00007f801d88b1a8
 id: 1,
 name: "Ümit Kayacık",
 email: "[FILTERED]",
 created_at: "2025-04-23 13:29:41.239473000 +0000",
 updated_at: "2025-04-23 13:29:41.239473000 +0000",
 password_digest: "[FILTERED]">


İki işlemi aynı anda yapan create metodunu kullanarak örnek kullanıcımızı ekledik. Şimdi gerçekleşen kaydı görmek için SQLite3 veri tabanını SQLite Browser uygulamasında açarsak kaydımızı göreceğiz. 


Gördüğünüz üzere password_digest sütununda yazılan değerin bizim verdiğimiz şifre ile alakası yok .

Bu arada kısa bir bilgi verelim herhangi bir anda veri tabanını sıfırlamak ve içini boşaltmak isterseniz şöyle yaparsınız

  1. Konsoldan çıkın
  2. rm -f storage/development.sqlite3 komutu ile mevcut veri tabanı dosyasını silin
  3. rails db:migrate komutu ile tüm migrasyonları tekrar aktif edin (bir kere çalıştırmak yeterli)
  4. Rails konsolu tekrar çalıştırın


>> user = User.find_by email: "umit@example.com"
...  
=>#<User:0x00007f4e99bdb048
...
>> user.password_digest
=> "$2a$12$b3w64djHhqucG/gig90JUuQCanpNeazlErbOC1.dX2S/elRQL.q8K"


Hash karıştırması yapılmış bu uzun değer aslında "foobar" string'ine karşılık bcrypt tarafından oluşturulan değerdir ve bu değerden "foobar" değerine dönmek imkansız gibi bir şey. 

Gelelim authenticate metodunu kullanma tekniğine. 

>> user.authenticate "geçersiz_şifre"
=> false
>> user.authenticate "foobaz"
=> false
>> user.authenticate "foobar"
=>
<User:0x00007f4e99bdb048
 id: 1,
 name: "Ümit Kayacık",
 email: "[FILTERED]",
 created_at: "2025-04-23 13:29:41.239473000 +0000",
 updated_at: "2025-04-23 13:29:41.239473000 +0000",
 password_digest: "[FILTERED]">


Kullanıcı nesnesinde authenticate metodu çağrılınca argümanda verilen şifre ile veri tabanı kaydındaki password_digest değeri eşleşirse bize kullanıcı nesnesini eşleşmezse false değer dönecektir. Bunu kullanarak ileride uygulamamızda kullanıcıların yetkilendirilmesi işlemlerini yapacağız nasipse. 




Bu kısımda öğrendiklerimiz

  • Migrasyonlar ile veri tabanı tablolarımızın yapısını değiştirebiliriz
  • Active Record sınıfı veri modellerini işlemek için birçok metoda sahiptir
  • Active Record geçerlik kuralları girdiğimiz verileri kısıtlamak için kriterler oluşturmak için kullanılır
  • Yaygın kullanılan geçerlilik kuralları , presence, length, ve format
  • Regexp ifadeler karmaşık ama çok kuvvetlidir
  • Veri tabanlarında kayıtlar için kısıtlamalar tanımlanabilir
  • Güvenli şifre alt yapısını has_secure_password metodu kullanarak oluşturabiliriz


Bu bölüm burada bitiyor, sonraki bölümde nasipse kayıt olma sayfasını gerçekleştireceğiz. Şimdilik kalın sağlıcakla..







Hiç yorum yok:

Yorum Gönder