← Writing

Ruby on Rails, Laravel: 起步走

前情提要 半年前,我在 Astrocamp 學習三個月的 Rails 開發, 結束後,誤打誤撞進入了以 Laravel 來開發的工作! 一開始的確有點擔心自己會不會吃不消,但慢慢發現這兩者在開發上有著很相似的味道, 慢慢記錄使用上的小小心得,或許之後都能用得上也說不定! 概念理解 在開發框架中,以下...

ruby on railslaravel

前情提要

半年前,我在 Astrocamp 學習三個月的 Rails 開發, 結束後,誤打誤撞進入了以 Laravel 來開發的工作! 一開始的確有點擔心自己會不會吃不消,但慢慢發現這兩者在開發上有著很相似的味道, 慢慢記錄使用上的小小心得,或許之後都能用得上也說不定!


概念理解

在開發框架中,以下幾個名詞都是很重要的概念:

  • MVC(Model-View-Controller)

MVC 是一種常見的軟體架構模式,將應用程式分為三個核心部分: 模型(Model)、視圖(View)和控制器(Controller)

在 Rails 和 Laravel 中,這種架構都被廣泛採用。

  • 模型(Model)負責處理應用程式的資料邏輯,
  • 視圖(View)負責呈現使用者介面,
  • 控制器(Controller)則是連接模型和視圖,並處理用戶的輸入。

透過 MVC 架構,開發者可以更清晰地組織和管理程式碼,實現代碼的重用性和可維護性。

  • RESTful 設計

RESTful 是一種設計風格,用於建立有效的、可擴展的 Web API。

基於 REST(Representational State Transfer)原則,強調使用統一資源標識符(Uniform Resource Identifiers,URI)來操縱資源。 透過 RESTful 設計,開發者可以定義清晰的路由結構、合理的 HTTP 方法使用和良好的資源命名,以實現易於維護和擴展的 API。

比方說,假設你在經營一家電子商務網站,希望用戶能夠通過 API 存取你的產品資訊。 使用 RESTful 設計,可以通過像 /products 這樣的 URI 來獲取所有產品資訊,配上合適的 HTTP 方法(例如 GET、POST、PUT、DELETE)來執行獲取、新增、更新或刪除產品。

  • Migration

Migration 是一種用於管理資料庫版本控制的機制,通過程式化的方式定義和更新資料庫結構。

在 Rails 和 Laravel 中,Migration 具有相似的概念和實現方式。開發者可以通過撰寫 Migration 檔案來定義資料庫結構的變化,然後使用框架提供的指令來執行 Migration,實現資料庫結構的異動。


Ruby on Rails

語言:Ruby / 作者: David Heinemeier Hansson 官方文件Rails Guides

  • 慣例優於設定(Convention over Configuration, CoC) Rails 強調慣例優於設定原則,提供預設的設定和工作流程,使得開發更加快速和一致。

Model 使用單數形式,例如 Article 代表一個文章模型; Controller 使用複數形式,並以"Controller"結尾,例如 ArticlesController

  • 不要做重複的事(Don’t Repeat Yourself, DRY) 把重複的部份抓出來,整理為一個方法、類別或模組。

  • 指令介面

使用 rails command line 來執行各種開發。

可以在終端機透過 rails --help,就可以看見所有可以用的 rails 指令: rails --help

  • 套件與套件管理

使用 Bundler 作為套件管理器,可以通過 Bundler 輕鬆管理 Gem 依賴關係。

Gem(是一個可以下載並安裝的 package。可以在開發上新增額外的功能。) Ex: Devise 提供完整的用戶註冊、登錄、忘記密碼等功能,直接快速完成會員系統。

bundle install

用於安裝所有必需的 gem 套件,包括指定版本的 gem 和其相依性。Bundler 會根據 Gemfile 文件中定義的依賴關係來安裝套件。 安裝的 gem 預設會放在 vendor/bundle 目錄下,並生成 Gemfile.lock 文件以確保後續安裝的 gem 版本與當前一致。

bundle update

如果要新增套件或更新,只需在 Gemfile 中添加套件的名稱和版本約束,並使用 bundle update

  • Object–relational mapping (ORM) 物件關聯映射

在 Rails 名為 Active Record 提供方便的資料庫操作方法, 使用 Ruby 與資料庫交互更加簡單和直觀。

實際以 code 來看 Active Record

建立一個 User 的資料表,其中包含 id、name 和 email 欄位, 並使用 Active Record 對 User 資料進行操作:

  • 在終端機使用 rails 指令,建立 User Model rails generate model User name:string email:string 可以簡寫成: rails g model User name email (*如果型別是 string 可以省略不寫)

id 是由 Rails 自動生成的欄位用於唯一識別每條記錄,也就是主鍵(primary key)。這個值是自動增長的,也就是每次新增記錄時都會自動增加。

指令輸入之後會生成與 User 相關的所有必要檔案, 包括 migration, model, test: rails g Model User

db/migrate/20240319034136_create_users.rb

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

         t.timestamps
      end
   end
end

app/models/user.rb

class User < ApplicationRecord
end

在繼續之前記得要跑一次,rails db:migrate, 這會執行所有尚未運行的資料庫遷移,並確保資料庫結構是最新的。 rails db:migrate

如何確認是否有沒有運行的 migration? rails db:migrate:status

Status   Migration ID    Migration Name
--------------------------------------------------
   up     20240319034136  Create users
   down   20240319073529  Drop users

*狀態是 "up",表示已執行,"down",表示尚未被執行。


接下來,以 rails console 也就是進入 Rails 的控制台, 在裡頭我們可以直接操作資料庫並使用 Active Record 來進行一系列查詢、新增、修改、刪除。

rails console

正式進入 rails console 裡: rails console

一但進入,就可以使用 Active Record 來與資料庫進行交互,以下為基本的操作方式:

建立一個新的 user

User.create(name: "Zinni Chang", email: "zinni@example.com")

:001 > User.create(name: "Zinni Chang", email: "zinni@example.com")
TRANSACTION (0.0ms)  begin transaction
User Create (0.5ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?)
[["name", "Zinni Chang"], ["email", "zinni@example.com"],
["created_at", "2024-03-19 12:49:29.883029"], ["updated_at", "2024-03-19 12:49:29.883029"]]
TRANSACTION (0.8ms)  commit transaction
=> 
#<User:0x00000001097b2258
... 

Rails 會啟動一個 TRANSACTION,並將我們的 create 動作放入這個 TRANSACTION 中,接著,Rails 執行 INSERT SQL 語句,將新的 user 插入到 users 資料表中,並將名稱、電子郵件地址以及建立時間和更新時間寫入資料庫。最後,TRANSACTION 成功!(也就是 commit transaction)。


查詢 user
  • 在 users 資料表的所有 user

    User.all

     :002 > User.all
      User Load (16.8ms)  SELECT "users".* FROM "users"
     => 
    [#<User:0x000000010a57b6a0
      id: 1,
      name: "Zinni Chang",
      email: "zinni@example.com",
      created_at: Fri, 19 Mar 2024 12:49:29.883029000 UTC +00:00,
      updated_at: Fri, 19 Mar 2024 12:49:29.883029000 UTC +00:00>]
    
  • 查詢特定的 user

    • find(id) 根據主鍵 (id) 來查找單一記錄。

    User.find(1)

    :003 > User.find(1)
    User Load (4.8ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
    => 
    #<User:0x000000010a5758e0
    id: 1,
    name: "Zinni Chang",
    email: "zinni@example.com",
    created_at: Fri, 19 Mar 2024 12:49:29.883029000 UTC +00:00,
    updated_at: Fri, 19 Mar 2024 12:49:29.883029000 UTC +00:00>
    
    • find_by() 根據指定的條件查找第一個匹配的記錄,這裡用欄位是 name 來尋找

    User.find_by(name: "Zinni Chang")

    :004 > User.find_by(name: "Zinni Chang")
    User Load (13.5ms)  SELECT "users".* FROM "users" WHERE "users"."name" = ? LIMIT ?  [["name", "Zinni Chang"], ["LIMIT", 1]]
    => 
    #<User:0x000000010a3b7e40
    id: 1,
    name: "Zinni Chang",
    email: "zinni@example.com",
    created_at: Fri, 19 Mar 2024 12:49:29.883029000 UTC +00:00,
    updated_at: Fri, 19 Mar 2024 12:49:29.883029000 UTC +00:00> 
    

更新 user

User.find(id).update(email: "new_zinni@example.com")

 :005 > User.find(1).update(email: "new_zinni@example.com")
  User Load (0.7ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  TRANSACTION (0.1ms)  begin transaction
  User Update (0.8ms)  UPDATE "users" SET "email" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["email", "new_zinni@example.com"], ["updated_at", "2024-03-19 05:36:18.867550"], ["id", 1]]
  TRANSACTION (0.9ms)  commit transaction
 => true 

刪除 user

User.destroy(id)

 :006 > User.destroy(1)
  User Load (1.3ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  TRANSACTION (0.0ms)  begin transaction
  User Destroy (0.4ms)  DELETE FROM "users" WHERE "users"."id" = ?  [["id", 1]]
  TRANSACTION (1.6ms)  commit transaction
 => 
#<User:0x0000000113d15510
 id: 9,
 name: "Zinni Chang",
 email: "new_zinni@example.com",
 created_at: Tue, 19 Mar 2024 05:34:41.488452000 UTC +00:00,
 updated_at: Tue, 19 Mar 2024 05:36:18.867550000 UTC +00:00> 

Laravel

語言:PHP / 作者:Taylor Otwell 官方文件Laravel

Laravel 相較 Rails 是一個較為年輕的框架,但他在短時間內迅速崛起, 成為 PHP 社群中最受歡迎的框架之一。 在看了一些文章與自己使用上,不難發現 Laravel 確實有 Rails 的感覺。

  • 與 Rails 不同,Laravel 在開發上沒有強制性的命名規則或預設設置, 這使得我們可以更靈活地運用框架,根據自己的需求和喜好進行配置和開發。

  • 指令介面

使用 artisan command line 來執行各種開發。

可以在終端機透過 php artisan --help,可以了解 Artisan 全面的說明與可以用的指令: php artisan --help

而 php artisan list 提供所有指令列表: php_artisan_list_1 php_artisan_list_2 php_artisan_list_3 php_artisan_list_4 php_artisan_list_5

  • 套件與套件管理

使用 Composer 作為套件管理器,可以通過 Composer 安裝、更新和管理依賴關係和第三方套件。

composer install

這指令使 Composer 將根據 composer.json 中定義的依賴關係,安裝所有必需的套件,包括指定版本的套件和其相依性; Composer 會將所有的套件下載並安裝到 vendor 目錄下,並生成 composer.lock 文件以確保後續安裝的套件版本與當前一致。

composer update

如果要新增套件或更新,只需在 composer.json 中添加套件的名稱和版本約束,並使用 composer update

  • Object–relational mapping (ORM) 物件關聯映射

在 Laravel 名為 Eloquent 提供方便的資料庫操作方法, 使用 PHP 與資料庫交互更加簡單和直觀。

實際以 code 來看 Eloquent

建立一個 User 的資料表,其中包含 id、name 和 email 欄位, 並使用 Eloquent 對 User 資料進行操作:

  • 在終端機使用 artisan 指令,建立 User Model php artisan make:model User -m 這指令會產生 Model 與 migration

    app/Models/User.php

    <?php
    
    namespace App\Models;
    
    use Illuminate\Database\Eloquent\Model;
    
    class User extends Model
    {
       /**
       * 資料表名稱
       *
       * @var string
       */
       protected $table = 'users';
    
       /**
       * 可以賦值的欄位
       *
       * @var array
       */
       protected $fillable = [
          'name', 'email',
       ];
    }
    

    database/migrations/2024_03_19_163819_create_users_table.php

    use Illuminate\Database\Migrations\Migration;
    use Illuminate\Database\Schema\Blueprint;
    use Illuminate\Support\Facades\Schema;
    
    class CreateUsersTable extends Migration
    {
       public function up()
       {
          Schema::create('users', function (Blueprint $table) {
                $table->id();
                $table->string('name');
                $table->string('email')->unique();
                $table->timestamps();
          });
       }
    
       public function down()
       {
          Schema::dropIfExists('users');
       }
    }
    

接下來,以 php artisan tinker, 在裡頭我們可以直接操作資料庫並使用 Eloquent 來進行一系列查詢、新增、修改、刪除。

在這之前記得要跑一次,php artisan migrate, 這會執行所有尚未運行的資料庫遷移,並確保資料庫結構是最新的。

如何確認是否有沒有運行的 migration? php artisan migrate:status

+------+-----------------------------------------------------------+-------+
| Ran? | Migration                                                 | Batch |
+------+-----------------------------------------------------------+-------+
| Yes  | 2024_03_10_000000_create_users_table                      | 1     |
| Yes  | 2024_03_12_000000_create_posts_table                      | 1     |
| No   | 2024_03_19_000000_add_column_to_users_table               |       |
+------+-----------------------------------------------------------+-------+

*狀態是 "Yes",表示已執行,"No",表示尚未被執行。

php artisan tinker

正式進入 Tinker 裡: php artisan tinker

一但進入,就可以使用 Eloquent 來與資料庫進行交互,以下為基本的操作方式:

建立一個新的 user

App\Models\User::create(['name' => 'Zinni Chang', 'email' => 'zinni@example.com']);

也可以簡寫: User::create(['name' => 'Zinni Chang', 'email' => 'zinni@example.com']);

> App\Models\User::create(['name' => 'Zinni Chang', 'email' => 'zinni@example.com']);
= App\Models\User {#6960
   name: "Zinni Chang",
   email: "zinni@example.com",
   updated_at: "2024-03-19 16:51:28",
   created_at: "2024-03-19 16:51:28",
   id: 1,
}
查詢 user
  • 在 users 資料表的所有 user

    User::all();

    > User::all();
    = Illuminate\Database\Eloquent\Collection {#7288
       all: [
          App\Models\User {#7287
          id: 1,
          name: "Zinni Chang",
          email: "zinni@example.com",
          email_verified_at: null,
          created_at: "2024-03-19 16:51:28",
          updated_at: "2024-03-19 16:51:28",
          },
       ],
    }
    
  • 查詢特定的 user

    • find(id) 根據主鍵 (id) 來查找單一記錄。

    User::find(1);

    > User::find(1);
    = App\Models\User {#7216
        id: 1,
        name: "Zinni Chang",
        email: "zinni@example.com",
        email_verified_at: null,
        created_at: "2024-03-19 16:51:28",
        updated_at: "2024-03-19 16:51:28",
      }
    
    • where() 根據指定的條件查找第一個匹配的記錄,這裡用欄位是 name 來尋找

    User::where('name', 'Zinni Chang')->first();

    > User::where('name', 'Zinni Chang')->first();
    = App\Models\User {#7070
       id: 1,
       name: "Zinni Chang",
       email: "zinni@example.com",
       email_verified_at: null,
       created_at: "2024-03-19 16:51:28",
       updated_at: "2024-03-19 16:51:28",
    }
    

更新 user

User::find(1)->update(['email' => 'new_zinni@example.com'])

> User::find(1)->update(['email' => 'new_zinni@example.com'])
= true

> User::find(1)
= App\Models\User {#7198
    id: 1,
    name: "Zinni Chang",
    email: "new_zinni@example.com",
    email_verified_at: null,
    created_at: "2024-03-19 05:41:54",
    updated_at: "2024-03-19 05:42:23",
  }

刪除 user

User::find(3)->delete()

> User::find(3)->delete()
= true

> User::find(3)
= null

資料庫關聯

資料庫關聯是指資料庫中不同表格之間的關聯。常見的關聯包括一對一、一對多和多對多。

在 Rails 和 Laravel,都可以通過定義模型之間的關係來實現資料庫關聯,也就可以輕鬆地在應用程式中操作相關聯的資料,並利用框架提供的方法來簡化資料庫查詢和操作。

  • 在 Rails 中,假設我們有兩個模型:UserPost, 他們之間是一對多的關係,也就是一個使用者可以擁有多篇文章。

    可以這樣定義:

    # app/models/user.rb
    class User < ApplicationRecord
       has_many :posts
    end
    
    # app/models/post.rb
    class Post < ApplicationRecord
       belongs_to :user
    end
    
  • 在 Laravel 中,也可以用類似的方式來定義這個關係。 假設同樣有 UserPost 兩個模型,

    可以這樣寫:

    // app/Models/User.php
    namespace App\Models;
    
    use Illuminate\Database\Eloquent\Model;
    
    class User extends Model
    {
       public function posts()
       {
          return $this->hasMany(Post::class);
       }
    }
    
    // app/Models/Post.php
    namespace App\Models;
    
    use Illuminate\Database\Eloquent\Model;
    
    class Post extends Model
    {
       public function user()
       {
          return $this->belongsTo(User::class);
       }
    }
    

這樣一來,在實際應用中,我們可以使用這些關係方法來輕鬆地查詢和操作相關聯的資料, 而不需要手動撰寫複雜的 SQL 查詢。

在 Rails 中:

# 建立一個使用者
user = User.create(name: "Zinni Chang")

# 為該使用者建立一篇文章
user.posts.create(title: "Laravel 新手村初登場", content: "RoR 與 Laravel")

# 查詢該使用者的所有文章
user.posts.each do |post|
  puts "Title: #{post.title}, Content: #{post.content}"
end

在 Laravel 中:

// 建立一個使用者
$user = User::create(['name' => 'Zinni Chang']);

// 為該使用者建立一篇文章
$post = $user->posts()->create(['title' => 'Laravel 新手村初登場', 'content' => 'RoR 與 Laravel']);

// 查詢該使用者的所有文章
foreach ($user->posts as $post) {
    echo "Title: " . $post->title . ", Content: " . $post->content . "\n";
}

Active Record 回調 與 Eloquent 事件

執行 Eloquent 事件(Laravel)和 Active Record 回調(Rails)都是在操作資料庫時執行額外邏輯的機制。 這兩種機制都允許在模型生命週期中的特定事件觸發時執行自定義的程式碼。

rails and laravel table

差異

  • 細分程度

    • Rails 提供了更細緻的回調級別,例如驗證前後、保存前後、建立前後、更新前後、刪除前後,這使得可以在更多細節層面上處理邏輯。
    • Laravel 的 Eloquent 事件相對較簡單,僅提供了模型被建立、更新或刪除的基本事件。
  • 用法差異

    • Rails 的 Active Record 回調是在模型類中定義的方法,這些方法會在模型的特定生命周期事件發生時自動被調用。 例如,在創建模型、更新模型或刪除模型時,可以定義相應的回調方法。
    • Laravel 的 Eloquent 事件需要通過註冊監聽器(Listener)來執行額外的邏輯,例如發送郵件。

    例如,如果要在建立新的 User 時發送歡迎郵件,可以這樣做:

    class User < ApplicationRecord
       after_create :send_welcome_email
    
       private
    
       def send_welcome_email
          # 發送歡迎郵件的邏輯
       end
    end
    

    after_create 回調會在建立新的 User 之後自動調用 send_welcome_email 方法。

    相比之下,在 Laravel 的 Eloquent 中,需要註冊事件監聽器來執行額外的邏輯:

    首先,建立一個 Eloquent 監聽器:

    <?php
    
    namespace App\Observers;
    
    use App\Models\User;
    
    class UserObserver
    {
       public function created(User $user)
       {
          // 寄送歡迎郵件的邏輯
          Mail::to($user->email)->send(new WelcomeEmail($user));
       }
    }
    

    然後,在 boot 方法中註冊該監聽器:

    <?php
    
    namespace App\Models;
    
    use Illuminate\Database\Eloquent\Model;
    use App\Observers\UserObserver;
    
    class User extends Model
    {
       protected static function boot()
       {
          parent::boot();
    
          static::observe(UserObserver::class);
       }
    }
    

    當建立新的 User 時,Eloquent 將會自動調用 created 方法,從而觸發相應的事件處理邏輯,這與 Rails 中 Active Record 回調的作用相似。

透過註冊回調方法,您可以在記錄的不同階段執行自定義的邏輯,從而擴展和定制模型的行為。

無論是在 Laravel 中的 Eloquent 事件還是在 Rails 中的 Active Record 回調,都提供了在模型生命週期中執行額外程式碼的方法。這些機制可以在資料庫操作的不同階段執行自定義邏輯,從而實現更高級別的業務邏輯或應用程式行為。


Summary

在 Ruby on Rails 中,許多功能和工作流程已經預先定義好了,例如路由、資料庫映射、文件結構等,開發者只需遵循這些約定就可以開發。 這種自動化配置和約定優於配置的方法有助於提高開發者的生產力,使得開發過程更加流暢。

相比之下,Laravel 並沒有像 Ruby on Rails 那樣嚴格遵循約定優於配置的原則。 雖然 Laravel 提供許多方便的功能和工具,開發者需要更多地進行手動配置來定義應用程序的行為和結構。 例如,Laravel 的路由、資料庫映射和文件結構沒有像 Rails 那樣嚴格的命名和結構約定,開發者可以自由地根據自己的偏好和需求進行設置和組織。 因此,可以說 Laravel 更加靈活。

Rails 和 Laravel 都是不錯的 Web 開發框架,具有各自的特點和優勢。 選擇使用哪個框架取決於項目需求、開發者技能和偏好等因素。