iT邦幫忙

2023 iThome 鐵人賽

DAY 17
0
自我挑戰組

富士大顆系列 第 17

vol. 17 Rails 來 new 一下!熟悉一下專案流程與指令吧

  • 分享至 

  • xImage
  •  

你好,我是富士大顆 Aiko
很久沒有 new 專案,面目可憎的 me
主要分幾個 part:

  • 規格需求
  • 環境設定
  • Models 與關聯
  • Controller 與 Route
  • View
  • Rails s

1. 了解規格需求:

  • 有一間叫做 Bookmarks 的書商,想要有一個 app 來展示他們的暢銷書榜
  • 主要的頁面就是排名的書籍們
  • 點擊書,會讓使用者到書的評論
    主要的功能確定了之後就可以規劃 model 長怎樣
    也可以思考有沒有其他的功能想做,例如要有會員登入嗎?

set up

2. rails new Project's name

通常在 new 專案的時候,我們會把一些設定也補充在這個指令裡面,像是資料庫、使用的前端框架 e.g. bootstrap, tailwind
會寫成:

$rails new MyProjectName-d postgresql --css tailwind --webpack=stimulus

這段指令包含:
-d postgresql:指定 PostgreSQL 作為資料庫(沒特別寫,預設是 SQLite)
--css tailwind:使用 Tailwind CSS 框架
--webpack=stimulus:使用 Stimulus 作為 JavaScript 框架,並且設定 Webpack

啊我們目前就簡單做,在終端機輸入:
$rails new cc_bookmarks

接著按下 enter 執行,Rails 就會長一長串檔案/資料夾,約莫 78 個,最貼心的是他也自動 git init
那預設最初你會在的位置是 main

接著我們到那個專案:
$cd cc_bookmarks

用 VSCode 打開:
$code.

或者有另外一個方法,當你 new 完專案之後就到你的資料夾,

拖曳這個資料夾到 VSCode 就好摟!

3. bundle install

安裝所有在 gemfile 裡的檔案
這些是目前檔案裡的 gem 列表:

source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby "3.2.2"

# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
gem "rails", "~> 7.0.8"

# The original asset pipeline for Rails [https://github.com/rails/sprockets-rails]
gem "sprockets-rails"

# Use sqlite3 as the database for Active Record
gem "sqlite3", "~> 1.4"

# Use the Puma web server [https://github.com/puma/puma]
gem "puma", "~> 5.0"

# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails]
gem "importmap-rails"

# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev]
gem "turbo-rails"

# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev]
gem "stimulus-rails"

# Build JSON APIs with ease [https://github.com/rails/jbuilder]
gem "jbuilder"

# Use Redis adapter to run Action Cable in production
# gem "redis", "~> 4.0"

# Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis]
# gem "kredis"

# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
# gem "bcrypt", "~> 3.1.7"

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem "tzinfo-data", platforms: %i[ mingw mswin x64_mingw jruby ]

# Reduces boot times through caching; required in config/boot.rb
gem "bootsnap", require: false

# Use Sass to process CSS
# gem "sassc-rails"

# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
# gem "image_processing", "~> 1.2"

group :development, :test do
  # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
  gem "debug", platforms: %i[ mri mingw x64_mingw ]
end

group :development do
  # Use console on exceptions pages [https://github.com/rails/web-console]
  gem "web-console"

  # Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler]
  # gem "rack-mini-profiler"

  # Speed up commands on slow machines / big apps [https://github.com/rails/spring]
  # gem "spring"
end

group :test do
  # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing]
  gem "capybara"
  gem "selenium-webdriver"

end

group 會分成 3 種環境:
development
test
production
所以可以根據需求,把 gem 分門別類到指定的環境裡,例如有些 gem 並不需要在 production 環境中使用,就把gem 放在 dev 或 text group ;一旦切換環境,使用 $bundle install 就會按照需求安裝。
預設都會是 development 喔!

記得經常執行 bundle update 來確保所有的 gems 都是最新版本!

models and associations

rails g model Model's name

ggenerate 的縮寫,所以你喜歡長的就用 generate

釐清一下需求,
這邊我們要的 model 基本就兩種:Book & Review

那我們就想一下兩個 model 的關聯性,正常情況下,Book 應該是 has_many :reviews 吧?
所以 Review belongs_to :books

那欄位名稱跟 data type 呢?

初步規劃,Model Book 的欄位會有:

  • a string column called title
  • a string column called author
  • a string column called description
  • a string column called publisher
  • an integer column called weeks_on_list
  • an integer column called rank_this_week

轉換成 code 就會是:

t.string :title
t.string :author
t.string :description
t.string :publisher
t.integer :weeks_on_list
t.integer :rank_this_week

在終端機輸入:
$rails g model Book
這邊有兩種新增欄位的方式:

    1. 結合指令$rails g model Book
      也就是變成 $rails g model Book title:string author:string description:string publisher:string weeks_on_list:integer rank_this_week:integer
      但是!這邊也有個比較簡潔的寫法,也就是當你的 datatype 是字串(string)的話,可以省略,因為 rails 預設資料型態是字串:
      $rails g model Book title author description publisher weeks_on_list:integer rank_this_week:integer
    1. 在指令執行完 $rails g model Book,編輯那個 migration
t.string :title
t.string :author
t.string :description
t.string :publisher
t.integer :weeks_on_list
t.integer :rank_this_week

那 model Review 的欄位呢?

  • a string column called author
  • a string column called comment
  • a references column to the Book model

所以一樣輸入 $rails g model Review
看你想用哪種方式編輯欄位

最後我的 model Review 長這樣:

class CreateReviews < ActiveRecord::Migration[7.0]
  def change
    create_table :reviews do |t|
      t.string :author
      t.string :comment
      t.references :book
      t.timestamps
    end
  end
end

比較特別要提的是 t.references :book
這會為你的資料表新增一個名為 book_id 的欄位,以及一個外鍵(foreign key)約束,以連結到 books 表格的 id 欄位,來建立表格之間的「關聯」
約束力再強一點,可以補充成:t.references :book, null: false, foreign_key: true

所以我在 model 寫好了關聯,也還是要加這一行嗎?
對。這樣比較完整。chat GPT 說:

如果你只寫 t.references :book,則不會有這些額外的約束。這可能會使你的資料庫狀態更容易出錯,因為它允>許 NULL 值和任意 book_id。
簡單地說,模型中的關聯和遷移檔案中的 t.references 是兩個相互補充的部分,兩者都是需要的。模型關聯負責應用邏輯,而 t.references 負責資料庫結構和約束。

ok
那我們再來編輯這兩個 model 檔,以完成關聯性:

class Book < ApplicationRecord
  has_many :reviews
end
class Review < ApplicationRecord
  belongs_to :book
end

$rails db:migrate

這個是比較古老的寫法:$bundle exec rake db:migrate

如果有 seed 檔的話(seed 就是預先要填入的資料,在開發階段可能會是一些假資料,看呈現的狀況),指令會是:
$bundle exec rake db:seed
同理可證 $rails db:migrate 也可行

執行完,就會有個 db/schema.rb 檔,記錄會自動更新所有 model 欄位
像這樣:

# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
# This file is the source Rails uses to define your schema when running `bin/rails
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
# be faster and is potentially less error prone than running all of your
# migrations from scratch. Old migrations may fail to apply correctly if those
# migrations use external dependencies or application code.
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[7.0].define(version: 2023_10_02_114904) do
  create_table "books", force: :cascade do |t|
    t.string "title"
    t.string "author"
    t.string "description"
    t.string "publisher"
    t.integer "weeks_on_list"
    t.integer "rank_this_week"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  create_table "reviews", force: :cascade do |t|
    t.string "author"
    t.string "comment"
    t.integer "book_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["book_id"], name: "index_reviews_on_book_id"
  end

end

Add a Controller and Routes

到了這個步驟,如果你輸入 $rails s
會是這個畫面:

然後,就沒有然後了
因為還沒有編輯 Controller & Route

rails g controller Controller's name(s)

注意這邊的 controller name 要是複數喔

一般來說 controller 名稱通常是 model 名稱的複數形式。
模型(model)是 Book,則對應的控制器(controller)一般會是 BooksController。

model 的要素是欄位
那 controller 呢?
當然就是 action 了

Rails 也有幾種不同的方式來新增 action

  • 結合指令rails g controller Controller's name(s)
    比方說,我要新增的 controller 裡要有 index 和 show
    指令就會是 $rails g controller Books index show
    這個方式的好處是,route 也會有這兩個 action 的 url 產生:
get 'books/index'
get 'books/show'

以及會自動生成 index 跟 show 分別對應的 view 檔案

  • 或者執行完指令 rails g controller Controller's name(s)
    再編輯 controller 檔
    這邊的 route 跟 view 就是要手動新增了

編輯 route

  1. 自動生成 route:

如果在生成 controller 時已經指定了 action(如 indexshow),則會在 config/routes.rb 檔案中自動生成對應的 route :

```ruby
get 'books/index'
get 'books/show'
```
  1. 手動新增 route:

如果之後手動在 controller 中添加了新的 action,則需要手動更新 config/routes.rb 檔案。
可以按照以下格式來新增:
get '<url>', to: '<controller>#<action>'

```ruby
get 'books/new', to: 'books#new'
```

或者想要一次設定多個標準的 CRUD(建立、讀取、更新、刪除),可以使用 `resources` 方法:

```ruby
resources :books, only: [:index, :show, :new, :create]
```

這會是 indexshownewcreate 這四個 action 相對應的 route。

  1. 訂製 Route:

    如果有特殊的 route 需求,也可以訂製,例如:

    get 'books/popular', to: 'books#popular'
    

這樣你要記得在 books controller 裡面新增 popular 的 action

也逐漸明白為什麼不選擇在指令執行一起新增 action
因為 route 還要改!
雖然 rails 幫你新增了:get 方法的 url
但我們這個專案要帶 id
所以會噴錯喔

查詢 route

使用指令$rails routes可以查到所有的 routes
如果太多,真的很難看的話
可以用指令 $rails routes | grep book
來篩選有 book 的 route

Add controller actions and views

controller editing

action 新增之後, block 會是空的,
所以我們要把 block 寫出來!

目前這個專案比較單純,只有兩個 actions

其實 action 要編輯的主要目的是要讓 view 知道,rend 的是什麼東西。
像這邊這樣的專案,index 就是所有的書
所以會這樣寫:

def index
  @books = Book.all
  end

這邊的 code 表示,我們用 @books 裝了 Book model 裡的所有資料
所以對照的 app/views/books/index.html.erb 就可以用 @books 來 rend 資料

show 比較複雜一點,但先不要緊張:

def show
  @book = Book.find(params[:id])
  @reviews = @book.reviews
  end

這邊預計是單本書的 show 但為了要找書,我們定位的是書的 id ,而這是唯一值所以不會搞錯書
params會幫我們抓取 url 裡的 id,啊因為 params 是 hash-like 的物件,要拿key id 的 value
就要用[:id]
然後會可以看到該書的所有 reviews
所以這邊使用了兩個實體變數,@book & @reviews
app/views/books/index.html.erb 這邊可以 rend 資料

比較要注意的是,通常因為我們需要新增修改原始資料,所以要 params 資料,因此要在這個 controller 底下新增 private def
params:

private

def book_params
  params.require(:book).permit(:title, :author, :description)
end

def set_book
  @book = Book.find(params[:id])
end

params 本身在 Rails 裡是一個 hash-like object,用來儲存從客戶端來的參數。
其中 book_params 方法通常用於 createupdate action,來過濾客戶端傳來的參數(也就是需要拿到 POST, PUT & PATCH 操作前的資料),防止有心人士透過所謂的 Mass Assignment 攻擊,使資料庫裡寫入不應該被寫入的欄位;
而 set_book 常常用於需要找到特定書籍的 action,如 showeditupdatedestroy
但因為我們這個專案還很單純,所以可以先不寫這兩個 private def

新增/編輯 view 檔

所以如果不選擇在指令執行就一起新增 action
這邊就要記得手動新增 view 檔
app/views/books在 books 資料夾裡面新增有的 action.html.erb
日後你有新的專案,也就是把 books 換掉,因為 books 這邊是我的 controller 名稱~

rails s

= rails server
啟動專案看看吧 連上 http://127.0.0.1:3000/

取消的指令是 control+C

接著就會是無限的除錯輪迴,但是不要害怕,問題都是可以解決的


以上是今天 CodeCademy 的專案實作練習!
同步一起放在 gitHub


上一篇
vol. 16 Rails 的導航員:routes.rb 「請往這邊走哦!」(下)
下一篇
vol. 18 Rails 的 render 和 redirect_to 有什麼差別?
系列文
富士大顆30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言