## 建立專案
```bash
# 不指定版本
$ rails new project_name
# 指定版本
$ rails _6.1.4.6_ new project_name
```
### 啟動
```bash
$ rails s
```
### 安裝套件
```bash
# 安裝特定套件
$ gem install rubocop
# 安裝 Gemfile 內列出內容
$ bundle install
```
## 2025/11/10 rails install
```bash
# 安裝 rbenv & ruby-build
brew install rbenv ruby-build
# 把 rbenv 加到 zsh 啟動腳本
echo 'eval "$(rbenv init - zsh)"' >> ~/.zshrc
source ~/.zshrc
rbenv -v
# 用 rbenv 安裝 & 切換 Ruby 版本,gem 安裝 Rails
rbenv install 3.3.0
rbenv global 3.3.0
gem install rails
rails -v
# 如果 gem install rails 顯示 ou don't have write permissions for the /Library/Ruby/Gems/2.6.0 directory
# 需要檢查當前路徑在哪裡,讓 rails 指向自己安裝的不是使用 mac 本身的 ruby
exec $SHELL -l
rbenv rehash
# 用已安裝好的 rails build project
rails new myapp
cd myapp
rails s
# 如果要指定版本
# 先安裝指定版本的 Rails gem
gem install rails -v 6.1.4.6
# 用該版本去產生專案
rails _6.1.4.6_ new myapp
cd myapp_6_1
bundle exec rails -v
```
## 指令
- rollback 會有風險,如果已經有資料存在,但必需得回滾的話,建議在新增一個 migration 修正。
- 新增欄位會產生一個空白的檔案,需要自行加上`add_column :articles, :photo, :string`,第一參數:資料表名稱、第二參數:欄位名稱、第三參數:資料型態。
```rb
# 建立 controller
$ rails g controller user
# 建立 model
$ rails g model User name:string age:integer
# 資料遷移
$ rails db:migrate
# 資料回滾
$ rails db:rollback
# 查看哪些 migration 已執行
# up:已執行、down:未執行
$ rails db:migrate:status
# 新增、修改、刪除資料表欄位
$ rails g migration add_phone_cache_to_user
$ rails g migration RenameDescription
```
## 數字 number
```rb
puts 10 / 3 # 3
puts 1 + 2 # 3
```
問題:要怎麼知道某個數字是不是奇數?
```rb
number = 10
if number % 2 == 0
puts "偶數"
else
puts "奇數"
end
```
或使用內建判斷做法
```rb
puts 10.odd? # false
puts 10.even? # true
```
問題:要怎麼做「四捨五入到小數點第二位」?
```rb
# 四捨五入到小數點第二位
puts 3.3333333.round(2) # 3.33
# 四捨五入
puts 3.834.round # 4
```
## 字串 string
字串安插
```rb
name = "Jeter"
age = 18
puts "你好,我是#{name},我今年#{age}。"
# 你好,我是Jeter,我今年18。
```
字串長度計算
```rb
puts "Jeter".size # 5
```
計算有多少個指定字元
```rb
# 先使用 split 方法拆解成陣列,在使用 count 計算
words = "Ut in aliquam mauris. Donec dolor quam, sagittis id efficitur vel, convallis vitae tortor"
puts words.split.count # 14
```
字串的大小寫轉換
```rb
puts "Jeter".upcase # 全大寫 JETER
puts "Jeter".downcase # 全小寫 jeter
puts "Jeter".swapcase # 自首小寫,其餘大寫 jETER
puts "jeter".capitalize # 自首大寫,其餘小寫 Jeter
```
想要知道某個字母在字串中共出現幾次
```rb
words = "Lorem Ipsum Dolor Sit Amet, Consectetur Adipiscing Elit."
puts words.count("i") # 5
# for loop
words = "Lorem Ipsum Dolor Sit Amet, Consectetur Adipiscing Elit."
count = 0
words.each_char do |word|
if word == "i"
count += 1
end
end
puts "字母 i 出現 #{count} 次。" # 字母 i 出現 5 次。
```
想知道字串是不是特定字元開題或結尾?或是包含指定字串或字元?
```rb
puts "Jeter".start_with?("J") # true
puts "Jeter".start_with?("j") # false
puts "Jeter".include?("J") # true
puts "Jeter".include?("j") # false
```
想把字串裡的某些字換成其它字
```rb
# sub 方法只會替換第一個遇到的字串。
puts "PHP is good, and I love PHP".sub(/PHP/, "Ruby")
# Ruby is good, and I love PHP
# gsub 方法會替換掉所有符合的字串。
puts "PHP is good, and I love PHP".gsub(/PHP/, "Ruby")
# Ruby is good, and I love Ruby
```
### empty?
檢查字串是否為空字串
```rb
puts "Jeter".empty? # false
puts " ".empty? # false,空白字元不算空字串
puts "".empty? # true
```
## 陣列 array
建立陣列的方法
```rb
p Array.new # []
p Array.new(5) # [nil, nil, nil, nil, nil]
p Array.new(5, "Ruby") # ["Ruby", "Ruby", "Ruby", "Ruby", "Ruby"]
```
通常會使用`[]`方式
```rb
friends = ["Jeter", "Jason", "Jay"]
```
陣列取用
```rb
friends = ["Jeter", "Jason", "Jay"]
puts friends[0] # Jeter
```
取得陣列第一或最後元素
```rb
friends = ["Jeter", "Jason", "Jay"]
puts friends.first # Jeter
puts friends.last # Jay
```
1. 把陣列`[1, 2, 3, 4, 5]`變成`[1, 3, 5, 7, 9]`
```rb
number = [1, 2, 3, 4, 5]
result = []
number.each do |num|
result << num * 2 - 1
end
p result # [1, 3, 5, 7, 9]
```
或是可以使用`map`
`map`方法會對陣列中的元素做某件事後再收集成新的陣列。
```rb
number = [1, 2, 3, 4, 5]
p number.map { |num| num * 2 - 1} # [1, 3, 5, 7, 9]
```
2. 計算從 1 加到 100 的總和
```rb
total = 0
(1..100).to_a.each do |i|
total = total + i
end
puts total # 5050
```
或是使用`*`展開
```rb
total = 0
[*1..100].each do |i|
total += i
end
puts total # 5050
```
`reduce`方法會對每個元素不斷的`互動(例子為相加)`,再把最後結果收集起來。
```rb
puts [*1..100].reduce(0) { |total, num| total + num } #. 5050
```
3. 請印出 1 ~ 100 數字中所有的單數
```rb
result = []
[*1..100].each do |num|
if num % 2 != 0
result << num
end
end
p result
# [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39, 41, 43, 45, 47, 49, 51, 53, 55, 57, 59, 61, 63, 65, 67, 69, 71, 73, 75, 77, 79, 81, 83, 85, 87, 89, 91, 93, 95, 97, 99]
```
`select`方法可篩選出符合條件的元素並收集成陣列。
```rb
p [*1..100].select { |num| num % 2 != 0 }
# [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39, 41, 43, 45, 47, 49, 51, 53, 55, 57, 59, 61, 63, 65, 67, 69, 71, 73, 75, 77, 79, 81, 83, 85, 87, 89, 91, 93, 95, 97, 99]
```
要計算奇數、偶數可利用數字物件內建方法
```rb
p [*1..100].select { |i| i.odd? }
```
4. 如果想要從 1 到 100 中隨便取 5 個不重複的亂數
```rb
puts [*1..100].sample(5)
```
## 範圍 range
.. 和 ... 差異
```rb
(1..5).to_a # [1, 2, 3, 4, 5]
(1...5).to_a # [1, 2, 3, 4]
```
範圍也可使用`each` or `map`
```rb
(1..5).each { |x| puts x } # 1 2 3 4 5
(1..5).map { |x| x * 2 } # [2, 4, 6, 8, 10]
```
取陣列內容也可使用範圍
```rb
languages = ["ruby", "python", "php", "javascript", "rails"]
languages[1..3] # ["python", "php", "javascript"]
```
## 雜湊 hash
Ruby 1.9 版本前後差異如下,但意思上是相同的。
```rb
old_hash = {:title => "Ruby", :price => 350}
new_hash = {title: "Ruby", price: 350}
puts old_hash == new_hash # true
```
Ruby 中要取得`hash`內容是需要使用`symbol`也就是`:`冒號,`:`和`" "`是不同東西,例如:`profile[:name]` and `profile["name"]`。
```rb
profile = {name: "Jeter", age: 10}
puts profile[:name]
```
1. 要怎麼把 Hash 裡的東西一個一個印出來?
```rb
profile = {name: "Jeter", age: 10}
p profile.keys # [:name, :age]
profile = {name: "Jeter", age: 10}
p profile.values # ["Jeter", 10]
```
如果要一個一個打印出來
```rb
profile = {name: "Jeter", age: 10}
profile.each do |elements|
p elements
end
# [:name, "Jeter"]
# [:age, 10]
```
如果要個別取得`key`and`value`
```rb
profile = {name: "Jeter", age: 10}
profile.each do |key, value|
puts key
puts value
end
# name
# Jeter
# age
# 10
```
## 符號 symbol
```rb
class Order
attr_reader :status
def initialize(items, status = :pending)
@items = items
@status = status
end
def complete
@status = :complete
end
end
order = Order.new(["item A", "item B", "item C"])
if order.status == :pending
puts "order is pending"
elsif order.status == :complete
puts "order is complete"
else
puts "not started"
end
# order is pending
# 如果呼叫 complete 會改變輸出內容
order.complete
if order.status == :pending
puts "order is pending"
elsif order.status == :complete
puts "order is complete"
else
puts "not started"
end
# order is complete
```
`symbol` and `string` 差異
前者無法改變內容,後者可以,所以可把`symbol`看成`不可變`的字串。
```rb
"hello"[0] = "k"
# k
:hello[0] = "k"
# `<main>': undefined method `[]=' for :hello:Symbol (NoMethodError)
```
`symbol` and `string` 是可以互相轉換的
## method and block
`method`定義和`python`是一樣的,都是使用`def`作為開頭,差別在`ruby`在結尾會加上`end`。
```rb
def hello(name)
puts "hello, #{name}"
end
hello("Jeter") # hello, Jeter
```
`ruby`可將方法使用`symbol`表示,依照下列程式碼來看可以把陣列中的`符號symbol`使用`send`方法直接呼叫,意思是會讓`:hello`這個符號去尋找對應的名為`hello`的方法然後執行。
```rb
def hello
puts "hello."
end
def goodbye
puts "goodbye."
end
methods = [:hello, :goodbye]
methods.each do |method|
send(method)
end
# hello.
# goodbye.
```
參數預設值
如果有帶參數給方法那就會使用參數內容,否則就會使用預設值 "hi"。
```rb
def hello(message = "hi")
"message: #{message}"
end
p hello("goodgood") # "message: goodgood"
p hello() # "message: hi"
```
在 Ruby 中`return`可被省略
```rb
def bmi(height, weight)
weight / height ** 2
end
puts bmi(1.60, 60) # 23.43
```
Ruby 定義方法時可以在命名後方加上`?`或是`!`,記住!只能加在命名後方。
`?` => 布林值
`!` => 改變原始資料結構
```rb
def hello?(age)
age >= 20
end
puts hello?(18) # false
```
```rb
# 未使用 !
number_list = [1, 2, 3, 4, 5]
reversed_list = number_list.reverse
p reversed_list # [5, 4, 3, 2, 1]
p number_list # [1, 2, 3, 4, 5]
# 使用 !
number_list = [1, 2, 3, 4, 5]
reversed_list = number_list.reverse!
p reversed_list # [5, 4, 3, 2, 1]
p number_list # [5, 4, 3, 2, 1]
```
Block
使用 yield 方法,暫時把控制權交棒給 Block,等 Block 執行結束後再把控制權交回來
```rb
def hello
puts "開始"
yield
puts "結束"
end
hello {
puts "這裡是 block"
# 開始
# 這裡是 block
# 結束
```
Block 並不是物件,所以無法單獨存在於 Ruby 語言中,所以需要依附在方法或是物件後方。
但如果使用`Proc`類別就可以將 Block 物件化,要使用時需要再該物件使用`call`
```rb
greeting = Proc.new { puts "hello world." }
greeting.call # hello world.
```
也可以帶參數
```rb
hello = Proc.new { |name| puts "hello, #{name}" }
hello.call("Jeter") # hello, Jeter
```
`Proc`呼叫方法

## 類別 class 和 模組 module
類別必需使用常數命名,有了類別後就可以使用`new`方法產生實體(instance)。
```rb
class Cat
def eat(food)
puts "#{food} 好好吃"
end
end
cat = Cat.new
cat.eat "肉泥" # 肉泥 好好吃
```
在 Ruby 中如果要透過`new`方法傳入參數,就必需得在物件中定義一個名為`initialize`的方法,不能取名為`init`、`__init__`等。
```rb
class Cat
def initialize(name, gender)
@name = name
@gender = gender
end
def hello
puts "hello, my name is #{@name}."
end
end
cat = Cat.new("Jeter", "female")
cat.hello # hello, my name is Jeter.
```
實體方法和類別方法
```rb
class Cat
def initialize(name, gender)
@name = name
@gender = gender
end
def hello
puts "hello, my name is #{@name}."
end
end
cat = Cat.new("Jeter", "female")
cat.hello # 作用在 cat 所產生的實體,所以 hello 為實體方法
class PostsController < ApplicationController
def index
# 取得所有 Post 資料,這裡的 all 方法作用在 Post 類別上,所以稱作類別方法
@posts = Post.all
end
end
```
定義類別方法有幾種方式:
1. 在方法前方加上`self`。
```rb
class Cat
def self.all
end
end
```
這樣就可使用`Cat.all`呼叫。
2.
```rb
class Cat
class << self
def all
end
end
end
```
方法的存取控制
- public => 預設公開,所有人都可以使用。
- protected => 可在同個類別內部使用,或是繼承該類別的子類別可使用。
- private => 只在類別內部使用。
```rb
class Cat
def eat
puts "好吃"
end
protected
def sleeping
puts "zzzz....."
end
private
def gossip
puts "hello!!!"
end
end
cat = Cat.new
cat.eat # 好吃
cat.sleeping # NoMethodError
cat.gossip # NoMethodError
```
但也不是`private`就無法被取得,利用`send`方法把要呼叫的方法當作參數傳遞就可以被取得。
```rb
cat.send(:gossip) # hello!!!
```
繼承
```rb
class Animal
def food(eat)
puts "#{eat}好好吃!"
end
end
class Cat < Animal
end
class Dog < Animal
end
food = Cat.new
food.food("罐頭") # 罐頭好好吃!
```
模組
不一定都需要使用繼承方式,利用`module`就可讓類別`include`後使用。
```rb
module Flyable
def fly
puts "我會飛!!!"
end
end
class Cat
include Flyable
end
cat = Cat.new
cat.fly # 我會飛!!!
```
## Routes
使用`resources`會讓 rails 自動產生 8 條路徑,並對應到 controller 的 7 個方法
```bash
Prefix Verb URI Pattern Controller#Action
users GET /users(.:format) users#index
POST /users(.:format) users#create
new_user GET /users/new(.:format) users#new
edit_user GET /users/:id/edit(.:format) users#edit
user GET /users/:id(.:format) users#show
PATCH /users/:id(.:format) users#update
PUT /users/:id(.:format) users#update
DELETE /users/:id(.:format) users#destroy
```
分別會是:
- index --> 使用者列表
- create --> 新增使用者
- new --> 新增使用者頁面
- show --> 檢視單一使用者
- update --> 更新資使用者
- edit --> 編輯使用者頁面
- destroy --> 刪除使用者
prefix 用意,在後面接上`_path`或`_url`後可以變成「產生相對應的路徑或網址」的 View Helper。如果是站內連結,通常會使用`_path`寫法來產生站內的路徑,例如:
```rb
products + path = products_path => /products
new_product + path = new_product_path => /products/new
edit_product + path = edit_product_path(2) => /products/2/edit
```
如果是使用`_url`則會產生完整的路徑,包括主機網域名稱:
```rb
products + url = products_url => http://kaochenlong.com/products
new_product + url = new_product_url => http://kaochenlong.com/products/new
edit_product + url = edit_product_url(2) => http://kaochenlong.com/products/2/edit
```
轉址
```rb
# 當使用者進入到 /me 這路徑會轉址到 redirect 頁面,當然路徑也是可以調整的,
# 例如:/me.php 這樣做看起來是個 php 做的網站,但其實是使用 Rails 做的,所以這部分滿特別!?
get "/me", to: redirect("https://www.highercloud.com.tw/")
```
在路徑顯示部分,如不需要一次產生 8 條路徑可以使用`only`或`except`來控制,例如:
```rb
# 只顯示 index and create routes
resources :users, only: [:index, :create]
# 這些不顯示
resources :users, except: [:new, :edit, :update, :destroy]
```
單數 resource 不會產生`:id`路徑且只會產生 7 條路徑(沒有 index)。
```rb
resource :user
# new_user GET /user/new(.:format) users#new
```
#### 單複數 resources 使用情境大致可分成,設計使用者只能夠 CRUD 自己的個人資料時使用單數 resource,如果是管理員可以 CRUD 使用者的個人資料時就使用複數 resources。
#### 大腸包小腸設計
可以理解成:
```bash
/users/1 --> 檢視使用者 1 號的資料
/users/1/posts --> 檢視使用者 1 號的所有文章
/users/1/posts/2 --> 檢視使用者 1 號的 2 號文章
/users/1/posts/2/edit --> 編輯使用者 1 號的 2號文章
```
```bash
Prefix Verb URI Pattern Controller#Action
user_posts GET /users/:user_id/posts(.:format) posts#index
POST /users/:user_id/posts(.:format) posts#create
new_user_post GET /users/:user_id/posts/new(.:format) posts#new
edit_user_post GET /users/:user_id/posts/:id/edit(.:format) posts#edit
user_post GET /users/:user_id/posts/:id(.:format) posts#show
PATCH /users/:user_id/posts/:id(.:format) posts#update
PUT /users/:user_id/posts/:id(.:format) posts#update
DELETE /users/:user_id/posts/:id(.:format) posts#destroy
users GET /users(.:format) users#index
POST /users(.:format) users#create
new_user GET /users/new(.:format) users#new
edit_user GET /users/:id/edit(.:format) users#edit
user GET /users/:id(.:format) users#show
PATCH /users/:id(.:format) users#update
PUT /users/:id(.:format) users#update
DELETE /users/:id(.:format) users#destroy
```
在路徑設計上因檢視特定資料、編輯、刪除、更新的操作並不一定得知道使用者是誰,所以可以不需要列出所有 routes,從下方設定來看,可看出文章部分因 index、new、create 需要知道 users_id,所以需要包在 users 內,但是 show、update、edit、destroy 並不一定需要知道 users_id 所以可以獨立在外層。
```rb
Rails.application.routes.draw do
resources :users do
resources :posts, only: [:index, :new, :create]
end
resources :posts, only: [:show, :edit, :update, :destroy]
end
```
後台網址設定,就是加一個 /admin 路徑在前方,想要做到這樣的網址設計可以直接在 resources 後面接上 path。
```rb
Rails.application.routes.draw do
resources :products, path: "/admin/products"
end
```
```bash
Prefix Verb URI Pattern Controller#Action
products GET /admin/products(.:format) products#index
POST /admin/products(.:format) products#create
new_product GET /admin/products/new(.:format) products#new
edit_product GET /admin/products/:id/edit(.:format) products#edit
product GET /admin/products/:id(.:format) products#show
PATCH /admin/products/:id(.:format) products#update
PUT /admin/products/:id(.:format) products#update
DELETE /admin/products/:id(.:format) products#destroy
```
但在做後台的時候,更常使用 namespace 方法來把 resources 包起來,產生後台的專屬路徑,這樣產生的專屬路徑會在前方加上 /admin。
```rb
Rails.application.routes.draw do
namespace :admin do
resources :products
resources :articles
end
end
```
```bash
Prefix Verb URI Pattern Controller#Action
admin_products GET /admin/products(.:format) admin/products#index
POST /admin/products(.:format) admin/products#create
new_admin_product GET /admin/products/new(.:format) admin/products#new
edit_admin_product GET /admin/products/:id/edit(.:format) admin/products#edit
admin_product GET /admin/products/:id(.:format) admin/products#show
PATCH /admin/products/:id(.:format) admin/products#update
PUT /admin/products/:id(.:format) admin/products#update
DELETE /admin/products/:id(.:format) admin/products#destroy
admin_articles GET /admin/articles(.:format) admin/articles#index
POST /admin/articles(.:format) admin/articles#create
new_admin_article GET /admin/articles/new(.:format) admin/articles#new
edit_admin_article GET /admin/articles/:id/edit(.:format) admin/articles#edit
admin_article GET /admin/articles/:id(.:format) admin/articles#show
PATCH /admin/articles/:id(.:format) admin/articles#update
PUT /admin/articles/:id(.:format) admin/articles#update
DELETE /admin/articles/:id(.:format) admin/articles#destroy
```
如果不希望後台路徑讓有心人士猜到,可以使用加入 path,這樣一來就把原本的 /admin 改變成自定義的內容,不易猜測。
```rb
Rails.application.routes.draw do
# namespace
namespace :admin, path: "saoidfai" do
resources :products
end
end
```
```bash
Prefix Verb URI Pattern Controller#Action
admin_products GET /saoidfai/products(.:format) admin/products#index
POST /saoidfai/products(.:format) admin/products#create
new_admin_product GET /saoidfai/products/new(.:format) admin/products#new
edit_admin_product GET /saoidfai/products/:id/edit(.:format) admin/products#edit
admin_product GET /saoidfai/products/:id(.:format) admin/products#show
PATCH /saoidfai/products/:id(.:format) admin/products#update
PUT /saoidfai/products/:id(.:format) admin/products#update
DELETE /saoidfai/products/:id(.:format) admin/products#destroy
```
## RESTful 設計
根據`CRUD`有不同的操作方式,例如:
```bash
# 取得所有會員列表
/users
# 取得 user id 為 123 的資料
/users/123
# 取得 user id 為 123 的貼文
/users/123/posts
# 編輯 user id 為 123 的資料
/users/123/edit
```
## Controller
定義邏輯的地方,當使用者進入到某頁面例如:`/hello_world`,這時會去`controller`尋找對應的控制器找尋對應方法,像是到`PagesController`中找尋`hello方法`,接著`Rails`會自動判斷到`app/views`資料夾中找`controller`名字目錄裡同名的檔案,因`controller`取名為`PagesController`所以會到`app/views/pages`資料夾中找`controller`裡和方法相同的檔案。
流程會是:`app/controller/pages_controller.rb --> app/views/pages/hello.html.erb`
```rb
# config/routes.rb
get "/hello_world", to: "pages#hello"
# resources 可自動產生 8 條不同的路徑,並對應到 controller 的 7 個方法
resources :posts
resources :users
# app/controllers/pages_controller.rb
class PagesController < ApplicationController
def hello
end
end
# app/views/pages/hello.html.erb
<h1>hello world</h1>
<h2>這是個 HTML 檔案!!!!!</h2>
```
## params
```rb
# app/controllers/pages_controller.rb
class PagesController < ApplicationController
def hello
render json: params
end
end
```
當使用者輸入:`http://127.0.0.1:3000/hello_world?name=Jeter&age=20`。
會得到以下像是 hash 的回傳內容。
```bash
{"name":"Jeter","age":"20","controller":"pages","action":"hello"}
```
如果只想取得 name 參數可以這樣操作
```rb
class PagesController < ApplicationController
def hello
render plain: params["name"]
end
end
```
不管是`get`or`post`方法傳遞的參數,都會被收集到`params`中。
## 練習:BMI 計算器
`form_tag`會被轉換成`<form> tag`、
`text_field_tag`會被轉換成`<input type="text"/>`、
`submit_tag`會被轉換成`<input type="submit"/>`
以上這些方法都統稱為`View Helper`。
```rb
# config/routes.rb
Rails.application.routes.draw do
get "bmi", to: "bmi#index"
post "bmi/result", to: "bmi#result"
end
# app/controllers/bmi_controller.rb
class BmiController < ApplicationController
def index
end
def result
height = params[:body_height].to_f / 100
weight = params[:body_weight].to_f
@bmi = (weight / (height * height)).round(2)
if @bmi >= 27
@bmi_info = "肥胖。"
elsif @bmi >= 24 && @bmi <27
@bmi_info = "過重。"
elsif @bmi >= 18.5 && @bmi < 24
@bmi_info = "健康。"
else
@bmi_info = "過輕。"
end
end
end
# app/views/bmi/index.html.erb
<h1>BMI 計算機</h1>
<%= form_tag '/bmi/result' do %>
身高:<%= text_field_tag 'body_height' %> 公分<br />
體重:<%= text_field_tag 'body_weight' %> 公斤<br />
<%= submit_tag "開始計算" %>
<% end %>
# app/views/bmi/result.html.erb
<h1>您的 BMI 為:<%= @bmi %></h1>
```
## form_for 和 form_tag
通常在`form_for`後面 block 裡會放`f`變數,是一種 FormBuilder物件,可以透過這個物件上的`text_field`、`text_area`、`submit`方法做出對應的`<input> tag`。
form_for:
- 可接一個 Model 物件參數
- 根據 Model 屬性自動生成輸入欄位
- 表單自動加入 Model 實例的值
form_tag:
- 不一定需要綁定到 Model,常用於通用表單
- 需要手動指定動作 URL
在`Controller`的`new`方法設定的實體變數`@candidate`,就是要給剛剛前面`form_for`用的。
`form_for`除了可以產生`<form> tag`之外,它的`action`,也就是當你按下送出按鈕要去的那個地方,會根據傳給它的這個物件是新的還是舊的而自己判斷。
在這裡因為是剛剛才做出來的,`form_for`會認為你現在是要做「新增」這件事。
## 資料關聯
- 一對一
`optional: true`表示允許 user 為填寫,如果沒有這樣做會產生 nil。
1. has_one 跟 belongs_to 方法需要同時設定嗎?
不一定,看實際需求而定。以 User 跟 Store 的例子來看,如果不需要「從 Store 反查 User」的功能,那 belongs_to 就不需要設定了。
```rb
# app/models/user.rb
class User < ApplicationRecord
has_one :store
end
# app/models/store.rb
class Store < ApplicationRecord
belongs_to :user, optional: true
end
```
- 一對多
```rb
# app/models/store.rb
class Store < ApplicationRecord
belongs_to :user
has_many :products
end
# app/models/product.rb
class Product < ApplicationRecord
belongs_to :store
end
```
- 多對多
沒辦法在兩邊的 model 定義`has_many`or`belongs_to`來做關聯,通常需要第三方資料表儲存兩邊的資訊。
1. `references`會自動加上索引(index),加速查詢速度。
2. 自動幫 model 加上 belongs_to
```bash
$ rails g model WareHouse store:references product:references
```
```rb
# app/models/ware_house.rb
class WareHouse < ApplicationRecord
belongs_to :store
belongs_to :product
end
# app/models/store.rb
class Store < ApplicationRecord
belongs_to :user
has_many :ware_houses
has_many :products, through: :ware_houses
end
# app/models/product.rb
class Product < ApplicationRecord
has_many :ware_houses
has_many :stores, through: :ware_houses
end
```
多對多關聯除了透過`has_many...through`還可以透過`has_and_belongs_to_many`,這樣就不需透過第三方 model,但還是得有第三方資料表。
```rb
# app/models/product.rb
class Product < ApplicationRecord
has_and_belongs_to_many :stores
end
# app/models/store.rb
class Store < ApplicationRecord
has_and_belongs_to_many :products
end
```
## 備註
`increment`方法是`Rails`中`Active Record`提供的一個方法,用於增加資料庫記錄中特定欄位的值並儲存到資料庫。
- increment
只在記憶體中增加值,並不會儲存進資料庫。
- increment!
增加值並儲存進資料庫。
```rb
@candidate.increment!(:votes)
```
## before_action 方法
在 controller 中有個 before_action 方法,指的是讓某個 action 在執行之前先做某件事。
```rb
class CandidatesController < ApplicationController
before_action :find_candidate, only: [:edit, :update, :destroy, :vote, :show]
def vote
if @candidatemodel 寫入到 vote_logs 資料表
@candidate.vote_logs.create(ip_address: request.remote_ip)
end
redirect_to candidates_path, notice: "完成投票。"
end
def index
@candidates = Candidate.order(id: :asc)
end
def new
@candidate = Candidate.new
end
def create
@candidate = Candidate.new(candidate_params)
if @candidate.save
redirect_to candidates_path, notice: "新增候選人成功。"
else
render :new
end
end
def show
end
def edit
end
def update
if @candidate.update(candidate_params)
redirect_to candidate_path, notice: "資料更新成功。"
else
render :edit
end
end
def destroy
if @candidate
@candidate.destroy
end
redirect_to candidate_path, notice: "候選人資料已刪除。"
end
# strong parameters,可對 params 內容資料做過濾,permit 方法表示只允許這些參數通過,其他的無視,算是開一個白名單概念。
private
def candidate_params
params.require(:candidate).permit(:name, :age, :party, :politics)
end
def find_candidate
# 透過 params 取得的所有資料型態都會是字串,如果要直接拿來做數學運算,需要使用 to_i 或 to_f 方法轉換型態
@candidate = Candidate.find_by(id: params[:id])
end
end
```
## yield
當 controller 在處理 view 時會先透過`app/views/layouts/application.html.erb`然後將`.hmtl.erb`的檔案填入`yield`中,這樣做主要是不需寫一堆重複的`HTML`。
1.`csrf_meta_tags`方法會在頁面上產生`<csrf-param>`跟`<csrf-token>`兩個`<meta>`標籤,用途主要是確保網站較不容易受到 CSRF(Cross-site request forgery)攻擊。
2. `stylesheet_link_tag`方法會轉換成 CSS 的`<link rel="stylesheet">`標籤。
3.`javascript_include_tag`方法會轉換成`JavaScript`的`<script>`標籤。
在`app/views/layouts/application.html.erb`定義好`yield`空格位置後,可在需要的模板使用`provide`填空。
```rb
# app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
<head>
<title><%= yield :my_title %></title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
</head>
<body>
<%= notice %>
<%= yield %>
</body>
</html>
# app/views/candidates/index.html.erb
<% provide :my_title, "你好,我是Title" %>
<h1>候選人列表</h1>
# app/views/candidates/new.html.erb
<% provide :my_title, "新增候選人" %>
<h1>新增候選人</h1>
<%= render "form", candidate: @candidate %>
<br />
<%= link_to "回候選人列表", candidates_path %>
```
## View Helper
`link_to`、`form_for`都算是一種 view helper
## attr_accessor
定義`getter`和`setter`方法,建立`可讀取`、`可設定`的屬性。
## validates
驗證機制,檢查資料是否符合規定。
## Model
Model 跟資料表通常是一對一沒錯,但並不是絕對的,有時候 Model 跟資料表一點關係都沒有。
### 慣例
Model 在命名上必需是大寫開頭的單數,例如:User。
資料表則是小寫或下底線的複數命名,例如:user_projects
## CRUD
### C
透過 model 新增一筆資料有`new`和`create`兩種方法,
使用`new`的話後續還要接上`save`才會把資料儲存進資料表,
如果使用`create`方法則不需要接上`save`。
### R
讀取資料,
`first & last` --> 取得第一筆或是最後一筆資料。
`find & find_by` --> 找到指定 id 的人。
差別是`find_by`方法如果找不到指定 id 的資料,僅會回傳 nil 物件,但`find`方法會直接產生`ActiveRecord::RecordNotFound`的例外訊息。
```rb
irb(main):001:0> Candidate.find_by(id: 9487)
Candidate Load (1.3ms) SELECT "candidates".* FROM "candidates" WHERE "candidates"."id" = $1 LIMIT $2 [["id", 9487], ["LIMIT", 1]]
=> nil
```
```rb
Candidate.first # 取得第一筆資料
Candidate.first(5) # 取得前三筆資料
Candidate.order(age: :desc).first # 取出年紀最大的候選人
Candidate.order(age: :desc).limit(3) # 限制取得筆數
Candidate.order(:age) # 小排到大
Candidate.find(1)
Candidate.find_by(id: 1)
Candidate.all # 取得所有資料
Candidate.where("age > 18") # 條件篩選
```
### U
更新資料常用的有`save`、`update`、`update_attribute`、`update_attributes`方法。
```rb
# 先找出要更新的人
user = Candidate.find_by(id: 1)
# 使用 save
candidate.name = "Jeter"
candidate.save
# 使用 update_attribute 方法更新單一欄位的值(注意:方法名字是單數)
Candidate.update_attribute(:name, "Jeter")
# 使用 update 更新資料,可一次更新多個欄位,且不需要再呼叫 save 方法
candidate.update(name: "Jeter", age: 18)
# 使用 update_attributes 方法
candidate.update_attributes(name: "Jeter", age: 18)
```
### D
刪除可以使用`delete`或`destroy`
```rb
# 先找出要操作的資料
user = Candidate.find_by(id: 1)
# 再把資料刪除
user.destroy # destroy 會有完整回呼(callback)
user.delete # 執行 SQL 的 DELETE FROM
```
## Migration
檔名符合某些字樣時,例如:add...to... 或是 remove...from ...,後面再加一些欄位就可以自動幫你產生一個寫好的 Migration 檔案。
```rb
$ rails g migration add_candidate_id_to_articles candidate_id:integer:index
```
```rb
class AddCandidateIdToArticles < ActiveRecord::Migration[5.0]
def change
add_column :articles, :candidate_id, :integer
add_index :articles, :candidate_id
end
end
```
## schema
這檔案內容是再執行`rails db:migrate`時自動產生的。
## seeds
要在建立資料表時順便產生初始資料時可以在`db/seeds.rb`檔案中做。
```rb
$ rails db:seed
```
## 資料驗證
- 在 Model 加上驗證
希望每篇文章的標題都是必填的。
```rb
# app/models/article.rb
class Article < ApplicationRecord
validates :title, presence: true
end
```
只有以下方式會觸發驗證:
- create
- create!
- save
- save!
- update
- update!
## 背景工作及排程
產生一個任務
```bash
rails g job user_confirm_email
```
## 寫測試
- 安裝測試工具
```bash
$ gem install rspec
```
- 在任意地方建立資料夾並建立檔案
在根目錄建立`TDD-test`資料夾,裡面有個`bank_account_spec.rb`檔案
```bash
TDD-test/bank_account_spec.rb
```
- 得知測試結果
```bash
$ rspec bank_account_spec.rb
```
- 測試結果
分為三種:
-- . 表示成功
-- F 表示失敗
-- * 表示待辦狀態pending